diff --git a/code/espurna/button.ino b/code/espurna/button.ino index dcef559da6..12147fc2b1 100644 --- a/code/espurna/button.ino +++ b/code/espurna/button.ino @@ -15,10 +15,17 @@ Copyright (C) 2016-2019 by Xose Pérez #include #include +#if FLOW_SUPPORT +class FlowButtonComponent; // forward declaration +#endif + typedef struct { DebounceEvent * button; unsigned long actions; unsigned int relayID; + #if FLOW_SUPPORT + std::vector flow_components; + #endif } button_t; std::vector _buttons; @@ -42,6 +49,35 @@ bool _buttonWebSocketOnReceive(const char * key, JsonVariant& value) { #endif +// ----------------------------------------------------------------------------- +// FLOW +// ----------------------------------------------------------------------------- + +#if FLOW_SUPPORT + +PROGMEM const char flow_data2[] = "Data"; +PROGMEM const char* const flow_data2_array[] = {flow_data2}; + +PROGMEM const FlowConnections flow_button_component = { + 0, NULL, + 1, flow_data2_array, +}; + +class FlowButtonComponent : public FlowComponent { + public: + FlowButtonComponent(JsonObject& properties) { + int button_id = properties["Button"]; + _buttons[button_id].flow_components.push_back(this); + } + + void buttonEvent(unsigned char event) { + JsonVariant data((int)event); + processOutput(data, 0); + } +}; + +#endif // FLOW_SUPPORT + int buttonFromRelay(unsigned int relayID) { for (unsigned int i=0; i < _buttons.size(); i++) { if (_buttons[i].relayID == relayID) return i; @@ -105,6 +141,12 @@ void buttonEvent(unsigned int id, unsigned char event) { } #endif + #if FLOW_SUPPORT + for (FlowButtonComponent* component : _buttons[id].flow_components) { + component->buttonEvent(event); + } + #endif + if (BUTTON_MODE_TOGGLE == action) { if (_buttons[id].relayID > 0) { relayToggle(_buttons[id].relayID - 1); @@ -122,11 +164,11 @@ void buttonEvent(unsigned int id, unsigned char event) { relayStatus(_buttons[id].relayID - 1, false); } } - + if (BUTTON_MODE_AP == action) { wifiStartAP(); } - + if (BUTTON_MODE_RESET == action) { deferredReset(100, CUSTOM_RESET_HARDWARE); } @@ -142,13 +184,13 @@ void buttonEvent(unsigned int id, unsigned char event) { wifiStartWPS(); } #endif // defined(JUSTWIFI_ENABLE_WPS) - + #if defined(JUSTWIFI_ENABLE_SMARTCONFIG) if (BUTTON_MODE_SMART_CONFIG == action) { wifiStartSmartConfig(); } #endif // defined(JUSTWIFI_ENABLE_SMARTCONFIG) - + #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE if (BUTTON_MODE_DIM_UP == action) { lightBrightnessStep(1); @@ -246,6 +288,17 @@ void buttonSetup() { wsOnReceiveRegister(_buttonWebSocketOnReceive); #endif + #if FLOW_SUPPORT + std::vector* buttons = new std::vector(); + for (unsigned int i=0; i < _buttons.size(); i++) { + buttons->push_back(String(i)); + } + + flowRegisterComponent("Button", &flow_button_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowButtonComponent(properties); })); + flowRegisterComponentValues("BUTTON_VALUES", buttons); + #endif + // Register loop espurnaRegisterLoop(buttonLoop); diff --git a/code/espurna/config/general.h b/code/espurna/config/general.h index 9c5f1967fb..93647c68df 100644 --- a/code/espurna/config/general.h +++ b/code/espurna/config/general.h @@ -1543,3 +1543,15 @@ #ifndef RFM69_IS_RFM69HW #define RFM69_IS_RFM69HW 0 #endif + +#ifndef FLOW_SUPPORT +#define FLOW_SUPPORT 0 +#endif + +#ifndef FLOW_SPIFFS_FILE +#define FLOW_SPIFFS_FILE "/flow.json" +#endif + +#ifndef FLOW_MQTT_TOPIC +#define FLOW_MQTT_TOPIC "flow" +#endif \ No newline at end of file diff --git a/code/espurna/config/progmem.h b/code/espurna/config/progmem.h index 05f4a8e003..29de646100 100644 --- a/code/espurna/config/progmem.h +++ b/code/espurna/config/progmem.h @@ -133,6 +133,9 @@ PROGMEM const char espurna_modules[] = #if WEB_SUPPORT "WEB " #endif + #if FLOW_SUPPORT + "FLOW " + #endif ""; //-------------------------------------------------------------------------------- @@ -348,3 +351,180 @@ PROGMEM const char* const magnitude_units[] = { }; #endif + +// ----------------------------------------------------------------------------- +// FLOW +// ----------------------------------------------------------------------------- + +#if FLOW_SUPPORT + +PROGMEM const char flow_library_json[] = + "{" + "\"Start\": " + "{" + "\"name\":\"Start\"," + "\"icon\":\"play\"," + "\"inports\":[]," + "\"outports\":[{\"name\":\"Data\",\"type\":\"bool\"}]," + "\"properties\":[{\"name\":\"Value\",\"type\":\"any\"}]" + "}" + ",\"Debug\": " + "{" + "\"name\":\"Debug\"," + "\"icon\":\"bug\"," + "\"inports\":[{\"name\":\"Data\",\"type\":\"any\"}]," + "\"outports\":[]," + "\"properties\":[{\"name\":\"Prefix\",\"type\":\"string\"}]" + "}" + ",\"Change\": " + "{" + "\"name\":\"Change\"," + "\"icon\":\"edit\"," + "\"inports\":[{\"name\":\"Data\",\"type\":\"any\"}]," + "\"outports\":[{\"name\":\"Data\",\"type\":\"any\"}]," + "\"properties\":[{\"name\":\"Value\",\"type\":\"any\"}]" + "}" + ",\"Math\": " + "{" + "\"name\":\"Math\"," + "\"icon\":\"plus-circle\"," + "\"inports\":[{\"name\":\"Input 1\",\"type\":\"any\"},{\"name\":\"Input 2\",\"type\":\"any\"}]," + "\"outports\":[{\"name\":\"Data\",\"type\":\"any\"}]," + "\"properties\":[{\"name\":\"Operation\",\"type\":\"list\"," + "\"values\":[\"+\",\"-\",\"*\",\"/\"]}]" + "}" + ",\"Compare\": " + "{" + "\"name\":\"Compare\"," + "\"icon\":\"chevron-circle-right\"," + "\"inports\":[{\"name\":\"Data\",\"type\":\"any\"},{\"name\":\"Test\",\"type\":\"any\"}]," + "\"outports\":[{\"name\":\"True\",\"type\":\"any\"},{\"name\":\"False\",\"type\":\"any\"}]," + "\"properties\":[{\"name\":\"Operation\",\"type\":\"list\",\"values\":[\"=\",\">\",\"<\"]},{\"name\":\"Test\",\"type\":\"any\"}]" + "}" + ",\"Delay\": " + "{" + "\"name\":\"Delay\"," + "\"icon\":\"pause\"," + "\"inports\":[{\"name\":\"Data\",\"type\":\"any\"},{\"name\":\"Reset\",\"type\":\"any\"}]," + "\"outports\":[{\"name\":\"Data\",\"type\":\"any\"}]," + "\"properties\":[{\"name\":\"Seconds\",\"type\":\"int\"}, {\"name\":\"Last only\",\"type\":\"bool\"}]" + "}" + ",\"Timer\": " + "{" + "\"name\":\"Timer\"," + "\"icon\":\"clock-o\"," + "\"inports\":[]," + "\"outports\":[{\"name\":\"Data\",\"type\":\"bool\"}]," + "\"properties\":[{\"name\":\"Seconds\",\"type\":\"int\"},{\"name\":\"Value\",\"type\":\"any\"}]" + "}" + ",\"Gate\": " + "{" + "\"name\":\"Gate\"," + "\"icon\":\"unlock\"," + "\"inports\":[{\"name\":\"Data\",\"type\":\"any\"}, {\"name\":\"State\",\"type\":\"bool\"}]," + "\"outports\":[{\"name\":\"Open\",\"type\":\"any\"}, {\"name\":\"Close\",\"type\":\"any\"}]," + "\"properties\":[]" + "}" + ",\"Hysteresis\": " + "{" + "\"name\":\"Hysteresis\"," + "\"icon\":\"line-chart\"," + "\"inports\":[{\"name\":\"Value\",\"type\":\"double\"}, {\"name\":\"Min\",\"type\":\"double\"}, {\"name\":\"Max\",\"type\":\"double\"}]," + "\"outports\":[{\"name\":\"Rise\",\"type\":\"double\"}, {\"name\":\"Fall\",\"type\":\"double\"}]," + "\"properties\":[{\"name\":\"Min\",\"type\":\"double\"}, {\"name\":\"Max\",\"type\":\"double\"}]" + "}" + ",\"Save setting\": " + "{" + "\"name\":\"Save setting\"," + "\"icon\":\"save\"," + "\"inports\":[{\"name\":\"Value\",\"type\":\"string\"}]," + "\"outports\":[]," + "\"properties\":[{\"name\":\"Name\",\"type\":\"string\"}]" + "}" + ",\"Load setting\": " + "{" + "\"name\":\"Load setting\"," + "\"icon\":\"database\"," + "\"inports\":[{\"name\":\"Name\",\"type\":\"string\"}]," + "\"outports\":[{\"name\":\"Value\",\"type\":\"string\"}]," + "\"properties\":[{\"name\":\"Default\",\"type\":\"string\"}]" + "}" +#if TERMINAL_SUPPORT + ",\"Terminal\": " + "{" + "\"name\":\"Terminal\"," + "\"icon\":\"terminal\"," + "\"inports\":[{\"name\":\"Run\",\"type\":\"any\"}, {\"name\":\"Command\",\"type\":\"string\"}]," + "\"outports\":[]," + "\"properties\":[{\"name\":\"Command\",\"type\":\"string\"}]" + "}" +#endif +#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE + ",\"Light\": " + "{" + "\"name\":\"Light\"," + "\"icon\":\"sun-o\"," + "\"inports\":[{\"name\":\"Color\",\"type\":\"string\"}, {\"name\":\"Brightness\",\"type\":\"int\"}]," + "\"outports\":[]," + "\"properties\":[]" + "}" +#endif + ",\"Relay\": " + "{" + "\"name\":\"Relay\"," + "\"icon\":\"lightbulb-o\"," + "\"inports\":[{\"name\":\"State\",\"type\":\"bool\"}, {\"name\":\"Toggle\",\"type\":\"any\"}]," + "\"outports\":[]," + "\"properties\":[{\"name\":\"Relay\",\"type\":\"list\",\"values\":[%RELAY_VALUES%]}]" + "}" +#if BUTTON_SUPPORT + ",\"Button\": " + "{" + "\"name\":\"Button\"," + "\"icon\":\"toggle-on\"," + "\"inports\":[]," + "\"outports\":[{\"name\":\"Data\",\"type\":\"int\"}]," + "\"properties\":[{\"name\":\"Button\",\"type\":\"list\",\"values\":[%BUTTON_VALUES%]}]" + "}" +#endif +#if MQTT_SUPPORT + ",\"MQTT subscribe\": " + "{" + "\"name\":\"MQTT subscribe\"," + "\"icon\":\"sign-out\"," + "\"inports\":[]," + "\"outports\":[{\"name\":\"Data\",\"type\":\"string\"}]," + "\"properties\":[{\"name\":\"Topic\",\"type\":\"string\"}]" + "}" + ",\"MQTT publish\": " + "{" + "\"name\":\"MQTT publish\"," + "\"icon\":\"sign-in\"," + "\"inports\":[{\"name\":\"Data\",\"type\":\"string\"}]," + "\"outports\":[]," + "\"properties\":[{\"name\":\"Topic\",\"type\":\"string\"}, {\"name\":\"Retain\",\"type\":\"bool\"}]" + "}" +#endif +#if SENSOR_SUPPORT + ",\"Sensor\": " + "{" + "\"name\":\"Sensor\"," + "\"icon\":\"thermometer-3\"," + "\"inports\":[]," + "\"outports\":[{\"name\":\"Data\",\"type\":\"double\"}]," + "\"properties\":[{\"name\":\"Sensor\",\"type\":\"list\",\"values\":[%SENSOR_VALUES%]}]" + "}" +#endif +#if SCHEDULER_SUPPORT + ",\"Schedule\": " + "{" + "\"name\":\"Schedule\"," + "\"icon\":\"calendar\"," + "\"inports\":[]," + "\"outports\":[{\"name\":\"Data\",\"type\":\"bool\"}]," + "\"properties\":[{\"name\":\"Time\",\"type\":\"time\"}, {\"name\":\"Weekdays\",\"type\":\"weekdays\"},{\"name\":\"Value\",\"type\":\"any\"}]" + "}" +#endif + "}"; + +#endif \ No newline at end of file diff --git a/code/espurna/config/prototypes.h b/code/espurna/config/prototypes.h index 684637c4f7..6d5df3ca97 100644 --- a/code/espurna/config/prototypes.h +++ b/code/espurna/config/prototypes.h @@ -224,3 +224,15 @@ bool wifiConnected(); #define thermostat_callback_f void * #endif +// ----------------------------------------------------------------------------- +// FLOW +// ----------------------------------------------------------------------------- + +#if FLOW_SUPPORT + #include "flow.h" + typedef std::function flow_component_factory_f; + void flowRegisterComponentValues(String placeholder, std::vector* values); +#else + #define FlowConnections void + #define flow_component_factory_f void * +#endif diff --git a/code/espurna/espurna.ino b/code/espurna/espurna.ino index 33a3afbae4..34165c3cd3 100644 --- a/code/espurna/espurna.ino +++ b/code/espurna/espurna.ino @@ -204,6 +204,10 @@ void setup() { #if THERMOSTAT_DISPLAY_SUPPORT displaySetup(); #endif + #if FLOW_SUPPORT + // after all other components are set up + flowSetup(); + #endif // 3rd party code hook diff --git a/code/espurna/flow.h b/code/espurna/flow.h new file mode 100644 index 0000000000..9129c614c3 --- /dev/null +++ b/code/espurna/flow.h @@ -0,0 +1,136 @@ +#pragma once + +#include +#include + +struct FlowConnections { + int inputsNumber; + const char* const* inputs; + int outputsNumber; + const char* const* outputs; +}; + +class FlowComponent { + private: + typedef struct { + FlowComponent* component; + int inputNumber; + } output_t; + + std::vector> _outputs; + + protected: + void processOutput(JsonVariant& data, int outputNumber) { + if (outputNumber < _outputs.size()) { + for (output_t output : _outputs[outputNumber]) + output.component->processInput(data, output.inputNumber); + } + } + + JsonVariant* clone(JsonVariant& data) { + if (data.is()) { + return new JsonVariant(data.as()); + } else if (data.is()) { + return new JsonVariant(data.as()); + } else if (data.is()) { + return new JsonVariant(data.as()); + } else if (data.is()) { + char *str = strdup(data.as()); + return new JsonVariant(str); + } else { + return new JsonVariant(data); + } + } + + String toString(JsonVariant& data) { + if (data.is()) { + return String(data.as()); + } else if (data.is()) { + return String(data.as(), 3); + } else if (data.is()) { + return String(data.as() ? "" : ""); + } else if (data.is()) { + return String(data.as()); + } else { + return String(); + } + } + + void release(JsonVariant* data) { + if (data == NULL) + return; + + if (data->is()) { + void* str = (void*)data->as(); + free(str); + } + delete data; + } + + public: + FlowComponent() { + } + + void addOutput(int outputNumber, FlowComponent* component, int inputNumber) { + if (outputNumber >= _outputs.size()) + _outputs.resize(outputNumber + 1); + _outputs[outputNumber].push_back({component, inputNumber}); + } + + virtual void processInput(JsonVariant& data, int inputNumber) { + } +}; + +typedef std::function flow_component_factory_f; + +class FlowComponentLibrary { + private: + std::map _connectionsMap; + std::map _factoryMap; + + public: + void addType(String name, const FlowConnections* connections, flow_component_factory_f factory) { + _connectionsMap[name] = connections; + _factoryMap[name] = factory; + } + + FlowComponent* createComponent(String& name, JsonObject& properties) { + flow_component_factory_f& factory = _factoryMap[name]; + return factory != NULL ? factory(properties) : NULL; + } + + int getInputNumber(String& name, String& input) { + const FlowConnections* connections = _connectionsMap[name]; + if (connections == NULL) + return -1; + + FlowConnections temp; + memcpy_P (&temp, connections, sizeof (FlowConnections)); + for (int i = 0; i < temp.inputsNumber; i++) { + if (strcmp_P(input.c_str(), temp.inputs[i]) == 0) + return i; + } + + return -1; + } + + int getOutputNumber(String& name, String& output) { + const FlowConnections* connections = _connectionsMap[name]; + if (connections == NULL) + return -1; + + FlowConnections temp; + memcpy_P (&temp, connections, sizeof (FlowConnections)); + for (int i = 0; i < temp.outputsNumber; i++) { + if (strcmp_P(output.c_str(), temp.outputs[i]) == 0) + return i; + } + + return -1; + } + + void clear() { + _connectionsMap.clear(); + _factoryMap.clear(); + } +}; diff --git a/code/espurna/flow.ino b/code/espurna/flow.ino new file mode 100644 index 0000000000..36737e2b49 --- /dev/null +++ b/code/espurna/flow.ino @@ -0,0 +1,742 @@ +/* + +FLOW MODULE + +Copyright (C) 2016-2018 by Xose P�rez + +*/ + +#if FLOW_SUPPORT + +#include +#include +#include +#include + +// ----------------------------------------------------------------------------- +// FLOW +// ----------------------------------------------------------------------------- + +#if !SPIFFS_SUPPORT +String _flow; +unsigned long _mqtt_flow_sent_at = 0; +#endif + +FlowComponentLibrary _library; +bool _flow_started = false; +std::map*> _flow_placeholder_values; + +void flowRegisterComponent(String name, const FlowConnections* connections, flow_component_factory_f factory) { + _library.addType(name, connections, factory); +} + +void flowRegisterComponentValues(String placeholder, std::vector* values) { + _flow_placeholder_values[placeholder] = values; +} + +AsyncWebServerResponse* flowGetConfigResponse(AsyncWebServerRequest *request) { + #if SPIFFS_SUPPORT + return request->beginResponse(SPIFFS, FLOW_SPIFFS_FILE, "text/json"); + #else + return request->beginResponse(200, "text/json", _flow); + #endif +} + +bool flowSaveConfig(char* data) { + bool result = false; + + #if SPIFFS_SUPPORT + File file = SPIFFS.open(FLOW_SPIFFS_FILE, "w"); + if (file) { + result = file.print(data); + file.close(); + } else { + DEBUG_MSG_P(PSTR("[FLOW] Error saving flow to file\n")); + } + #elif MQTT_SUPPORT + result = mqttConnected(); + _flow = String(data); + if (result) { + _mqtt_flow_sent_at = millis(); + mqttSendRaw(mqttTopic("flow", true).c_str(), data, true); + } + else { + DEBUG_MSG_P(PSTR("[FLOW] Error publishing flow because MQTT is disconnected\n")); + } + #else + _flow = String(data); + DEBUG_MSG_P(PSTR("[FLOW] Error saving flow\n")); + #endif + + return result; +} + +String flowLibraryProcessor(const String& var) +{ + std::vector* values = _flow_placeholder_values[var]; + if (values != NULL) { + String result; + for (String& value : *values) { + if (result.length() > 0) result += ","; + result += "\"" + value + "\""; + } + return result; + } + return String(); +} + +void flowStart() { + if (_flow_started) { + DEBUG_MSG_P(PSTR("[FLOW] Started already\n")); + return; + } + + DEBUG_MSG_P(PSTR("[FLOW] Starting\n")); + + #if SPIFFS_SUPPORT + File source = SPIFFS.open(FLOW_SPIFFS_FILE, "r"); + if (!source) { + DEBUG_MSG_P(PSTR("[FLOW] No flow file found\n")); + return; + } + #else + String& source = _flow; + #endif + + DynamicJsonBuffer jsonBuffer; + JsonObject& root = jsonBuffer.parseObject(source); + if (root.success()) _flowStart(root); + else DEBUG_MSG_P(PSTR("[FLOW] Error: flow cannot be parsed as correct JSON\n")); + + #if SPIFFS_SUPPORT + source.close(); + #endif + + _library.clear(); // clear library to release memory + _flow_started = true; +} + +void _flowAddConnection(std::map& components, std::map& componentsNames, + String& srcProcess, String& srcPort, String& tgtProcess, String& tgtPort) { + FlowComponent* srcComponent = components[srcProcess]; + if (srcComponent == NULL) { + DEBUG_MSG_P(PSTR("[FLOW] Error: component ID='%s' is not registered\n"), srcProcess.c_str()); + return; + } + + FlowComponent* tgtComponent = components[tgtProcess]; + if (tgtComponent == NULL) { + DEBUG_MSG_P(PSTR("[FLOW] Error: component ID='%s' is not registered\n"), tgtProcess.c_str()); + return; + } + + int srcNumber = _library.getOutputNumber(componentsNames[srcProcess], srcPort); + if (srcNumber < 0) { + DEBUG_MSG_P(PSTR("[FLOW] Error: component '%s' has no output named '%s'\n"), componentsNames[srcProcess].c_str(), srcPort.c_str()); + return; + } + + int tgtNumber = _library.getInputNumber(componentsNames[tgtProcess], tgtPort); + if (tgtNumber < 0) { + DEBUG_MSG_P(PSTR("[FLOW] Error: component '%s' has no input named '%s'\n"), componentsNames[tgtProcess].c_str(), tgtPort.c_str()); + return; + } + + srcComponent->addOutput(srcNumber, tgtComponent, tgtNumber); +} + +void _flowStart(JsonObject& data) { + std::map components; + std::map componentsNames; + + JsonVariant processes = data.containsKey("P") ? data["P"] : data["processes"]; + if (processes.is()) { + for (auto process_kv: processes.as()) { + String id = process_kv.key; + JsonObject& value = process_kv.value; + + String componentName = value.containsKey("C") ? value["C"] : value["component"]; + JsonObject& metadata = value.containsKey("M") ? value["M"] : value["metadata"]; + JsonObject& properties = metadata.containsKey("R") ? metadata["R"] : metadata["properties"]; + + FlowComponent* component = _library.createComponent(componentName, properties); + + if (component != NULL) { + components[id] = component; + componentsNames[id] = componentName; + } else { + DEBUG_MSG_P(PSTR("[FLOW] Error: component '%s' is not registered\n"), componentName.c_str()); + } + } + } else { + for (JsonArray& process: processes.as()) { + String id = process[0]; + String componentName = process[1]; + JsonObject& properties = process[5]; + + FlowComponent* component = _library.createComponent(componentName, properties); + + if (component != NULL) { + components[id] = component; + componentsNames[id] = componentName; + } else { + DEBUG_MSG_P(PSTR("[FLOW] Error: component '%s' is not registered\n"), componentName.c_str()); + } + } + } + + JsonArray& connections = data.containsKey("X") ? data["X"] : data["connections"]; + for (JsonVariant& connectionVariant: connections) { + if (connectionVariant.is()) { + JsonObject& connection = connectionVariant.as(); + JsonObject& src = connection.containsKey("S") ? connection["S"] : connection["src"]; + JsonObject& tgt = connection.containsKey("T") ? connection["T"] : connection["tgt"]; + + String srcProcess = src.containsKey("I") ? src["I"] : src["process"]; + String srcPort = src.containsKey("N") ? src["N"] : src["port"]; + String tgtProcess = tgt.containsKey("I") ? tgt["I"] : tgt["process"]; + String tgtPort = tgt.containsKey("N") ? tgt["N"] : tgt["port"]; + + _flowAddConnection(components, componentsNames, srcProcess, srcPort, tgtProcess, tgtPort); + } else { + JsonArray& connection = connectionVariant.as(); + + String srcProcess = connection[0]; + String srcPort = connection[1]; + String tgtProcess = connection[2]; + String tgtPort = connection[3]; + + _flowAddConnection(components, componentsNames, srcProcess, srcPort, tgtProcess, tgtPort); + } + } +} + +// ----------------------------------------------------------------------------- +// Start component +// ----------------------------------------------------------------------------- + +PROGMEM const char flow_data[] = "Data"; +PROGMEM const char* const flow_data_array[] = {flow_data}; + +PROGMEM const FlowConnections flow_start_component = { + 0, NULL, + 1, flow_data_array, +}; + +class FlowStartComponent : public FlowComponent { + private: + JsonVariant *_value; + Ticker _startTicker; + + public: + FlowStartComponent(JsonObject& properties) { + JsonVariant value = properties["Value"]; + _value = clone(value); + + _startTicker.once_ms(100, onStart, this); + } + + static void onStart(FlowStartComponent* component) { + component->processOutput(*component->_value, 0); + } +}; + +// ----------------------------------------------------------------------------- +// Debug component +// ----------------------------------------------------------------------------- + +PROGMEM const FlowConnections flow_debug_component = { + 1, flow_data_array, + 0, NULL, +}; + +PROGMEM const char flow_debug_string[] = "[FLOW DEBUG] %s%s\n"; + +class FlowDebugComponent : public FlowComponent { + private: + String _prefix; + + public: + FlowDebugComponent(JsonObject& properties) { + const char * prefix = properties["Prefix"]; + _prefix = String(prefix != NULL ? prefix : ""); + } + + virtual void processInput(JsonVariant& data, int inputNumber) { + String s = toString(data); + DEBUG_MSG_P(flow_debug_string, _prefix.c_str(), s.c_str()); + } +}; + +// ----------------------------------------------------------------------------- +// Change component +// ----------------------------------------------------------------------------- + +PROGMEM const FlowConnections flow_change_component = { + 1, flow_data_array, + 1, flow_data_array, +}; + +class FlowChangeComponent : public FlowComponent { + private: + JsonVariant* _value; + + public: + FlowChangeComponent(JsonObject& properties) { + JsonVariant value = properties["Value"]; + _value = clone(value); + } + + virtual void processInput(JsonVariant& data, int inputNumber) { + processOutput(*_value, 0); + } +}; + +// ----------------------------------------------------------------------------- +// Math component +// ----------------------------------------------------------------------------- + +PROGMEM const char flow_input1[] = "Input 1"; +PROGMEM const char flow_input2[] = "Input 2"; +PROGMEM const char* const flow_inputs_array[] = {flow_input1, flow_input2}; + +PROGMEM const FlowConnections flow_math_component = { + 2, flow_inputs_array, + 1, flow_data_array, +}; + +class FlowMathComponent : public FlowComponent { + private: + String _operation; + JsonVariant *_input1, *_input2; + + public: + FlowMathComponent(JsonObject& properties) { + const char * operation = properties["Operation"]; + _operation = String(operation != NULL ? operation : ""); + } + + virtual void processInput(JsonVariant& data, int inputNumber) { + if (inputNumber == 0) { + if (_input1 != NULL) release(_input1); + _input1 = clone(data); + } else if (inputNumber == 1) { + if (_input2 != NULL) release(_input2); + _input2 = clone(data); + } + + if (_input1 != NULL && _input2 != NULL) { + if (_input1->is()) { + int i1 = _input1->as(); + int i2 = _input2->as(); + JsonVariant r( + _operation.equals("+") ? i1 + i2 : + _operation.equals("-") ? i1 - i2 : + _operation.equals("*") ? i1 * i2 : + /*_operation.equals("/") ?*/ i1 / i2 + ); + processOutput(r, 0); + } else if (_input1->is()) { + double d1 = _input1->as(); + double d2 = _input2->as(); + JsonVariant r( + _operation.equals("+") ? d1 + d2 : + _operation.equals("-") ? d1 - d2 : + _operation.equals("*") ? d1 * d2 : + /*_operation.equals("/") ?*/ d1 / d2 + ); + processOutput(r, 0); + } else if (_input1->is()) { + // only + is supported + String s(_input1->as()); + s += toString(*_input2); + + JsonVariant r(s.c_str()); + processOutput(r, 0); + } else if (_input1->is()) { + bool b1 = _input1->as(); + bool b2 = _input2->as(); + JsonVariant r( + _operation.equals("+") ? b1 || b2 : + _operation.equals("-") ? !b1 : // NOT for first only + _operation.equals("*") ? b1 && b2 : + /*_operation.equals("/") ?*/ (b1 && !b2) || (!b1 && b2) // XOR + ); + processOutput(r, 0); + } + } + } + + static void reg() { + flowRegisterComponent("Math", &flow_math_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowMathComponent(properties); })); + } +}; + +// ----------------------------------------------------------------------------- +// Compare component +// ----------------------------------------------------------------------------- + +PROGMEM const char flow_true[] = "True"; +PROGMEM const char flow_false[] = "False"; +PROGMEM const char flow_test[] = "Test"; +PROGMEM const char* const flow_compare_inputs[] = {flow_data, flow_test}; +PROGMEM const char* const flow_compare_outputs[] = {flow_true, flow_false}; + +PROGMEM const FlowConnections flow_compare_component = { + 2, flow_compare_inputs, + 2, flow_compare_outputs, +}; + +class FlowCompareComponent : public FlowComponent { + private: + String _operation; + JsonVariant *_data, *_test; + + public: + FlowCompareComponent(JsonObject& properties) { + const char * operation = properties["Operation"]; + _operation = String(operation != NULL ? operation : ""); + + JsonVariant test = properties["Test"]; + _test = clone(test); + } + + virtual void processInput(JsonVariant& data, int inputNumber) { + if (inputNumber == 0) { + if (_data != NULL) release(_data); + _data = clone(data); + } else if (inputNumber == 1) { + if (_test != NULL) release(_test); + _test = clone(data); + } + + if (_data != NULL && _test != NULL) { + bool r; + if (_data->is()) { + double d1 = _data->as(); + double d2 = _test->as(); + r = _operation.equals("=") ? d1 == d2 : + _operation.equals(">") ? d1 > d2 : + /*_operation.equals("<") ?*/ d1 < d2 + ; + } else if (_data->is()) { + const char *s1 = _data->as(); + const char *s2 = _test->as(); + int cmp = s1 == NULL ? (s2 == NULL ? 0 : -1) : + s2 == NULL ? 1 : + strcmp(s1, s2); + r = _operation.equals("=") ? cmp == 0 : + _operation.equals(">") ? cmp > 0 : + /*_operation.equals("<") ?*/ cmp < 0 + ; + } else if (_data->is()) { + bool b1 = _data->as(); + bool b2 = _test->as(); + r = _operation.equals("=") ? b1 == b2 : + _operation.equals(">") ? b1 > b2 : + /*_operation.equals("<") ?*/ b1 < b2 + ; + } + processOutput(*_data, r ? 0 : 1); + } + } + + static void reg() { + flowRegisterComponent("Compare", &flow_compare_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowCompareComponent(properties); })); + } +}; + +// ----------------------------------------------------------------------------- +// Delay component +// ----------------------------------------------------------------------------- + +PROGMEM const char flow_reset[] = "Reset"; +PROGMEM const char* const flow_delay_inputs[] = {flow_data, flow_reset}; + +PROGMEM const FlowConnections flow_delay_component = { + 2, flow_delay_inputs, + 1, flow_data_array, +}; + +class FlowDelayComponent : public FlowComponent { + private: + struct scheduled_task_t { + FlowDelayComponent *component; + Ticker *ticker; + JsonVariant *data; + }; + + long _time; + bool _lastOnly; + int _queueSize = 0; + long _skipNumber = 0; + + public: + FlowDelayComponent(JsonObject& properties) { + _time = 1000 * (int)properties["Seconds"]; + _lastOnly = properties["Last only"]; + } + + virtual void processInput(JsonVariant& data, int inputNumber) { + if (inputNumber == 0) { // data + Ticker *ticker = new Ticker(); + scheduled_task_t *task = new scheduled_task_t(); + task->component = this; + task->ticker = ticker; + task->data = clone(data); + + ticker->once_ms(_time, onDelay, task); + + _queueSize++; + } else { // reset + _skipNumber = _queueSize; + } + } + + static void onDelay(scheduled_task_t *task) { + task->component->onDelayImpl(task->data); + + task->ticker->detach(); + free(task->ticker); + free(task); + } + + void onDelayImpl(JsonVariant *data) { + if (_skipNumber == 0) { + if (!_lastOnly || _queueSize == 1) + processOutput(*data, 0); + } else { + _skipNumber--; + } + + _queueSize--; + + release(data); + } +}; + +// ----------------------------------------------------------------------------- +// Timer component +// ----------------------------------------------------------------------------- + +PROGMEM const FlowConnections flow_timer_component = { + 0, NULL, + 1, flow_data_array, +}; + +PROGMEM const char flow_incorrect_timer_delay[] = "[FLOW] Incorrect timer delay: %i\n"; + +class FlowTimerComponent : public FlowComponent { + private: + JsonVariant *_value; + Ticker _ticker; + + public: + FlowTimerComponent(JsonObject& properties) { + JsonVariant value = properties["Value"]; + _value = clone(value); + + int seconds = properties["Seconds"]; + int period = 1000 * (int)seconds; + + if (period > 0) { + _ticker.attach_ms(period, onSchedule, this); + } else { + DEBUG_MSG_P(flow_incorrect_timer_delay, seconds); + } + } + + static void onSchedule(FlowTimerComponent *component) { + component->processOutput(*component->_value, 0); + } +}; + +// ----------------------------------------------------------------------------- +// Gate component +// ----------------------------------------------------------------------------- + +PROGMEM const char flow_state[] = "State"; +PROGMEM const char flow_open[] = "Open"; +PROGMEM const char flow_close[] = "Close"; +PROGMEM const char* const flow_gate_component_inputs[] = {flow_data, flow_state}; +PROGMEM const char* const flow_gate_component_outputs[] = {flow_open, flow_close}; +PROGMEM const FlowConnections flow_gate_component = { + 2, flow_gate_component_inputs, + 2, flow_gate_component_outputs, +}; + +class FlowGateComponent : public FlowComponent { + private: + bool _state = true; + + public: + FlowGateComponent(JsonObject& properties) { + } + + virtual void processInput(JsonVariant& data, int inputNumber) { + if (inputNumber == 0) { // data + processOutput(data, _state ? 0 : 1); + } else { // state + _state = data.as(); + } + } +}; + +// ----------------------------------------------------------------------------- +// Hysteresis component +// ----------------------------------------------------------------------------- + +PROGMEM const char flow_value[] = "Value"; +PROGMEM const char flow_min[] = "Min"; +PROGMEM const char flow_max[] = "Max"; +PROGMEM const char flow_rise[] = "Rise"; +PROGMEM const char flow_fall[] = "Fall"; +PROGMEM const char* const flow_hysteresis_component_inputs[] = {flow_value, flow_min, flow_max}; +PROGMEM const char* const flow_hysteresis_component_outputs[] = {flow_rise, flow_fall}; +PROGMEM const FlowConnections flow_hysteresis_component = { + 3, flow_hysteresis_component_inputs, + 2, flow_hysteresis_component_outputs, +}; + +class FlowHysteresisComponent : public FlowComponent { + private: + bool _state = false; + double _min = NAN; + double _max = NAN; + double _value = NAN; + + public: + FlowHysteresisComponent(JsonObject& properties) { + JsonVariant min = properties["Min"]; + JsonVariant max = properties["Max"]; + _min = min.success() && min.is() ? min.as() : NAN; + _max = max.success() && max.is() ? max.as() : NAN; + } + + virtual void processInput(JsonVariant& data, int inputNumber) { + if (inputNumber == 0) { // value + _value = data.as(); + if ((_state && _value >= _max) || (!_state && _value <= _min)) { + _state = !_state; + processOutput(data, _state ? 1 : 0); + } + } else if (inputNumber == 1) { // min + _min = data.as(); + if (!_state && _value <= _min) { + _state = true; + JsonVariant value(_value); + processOutput(value, 1); + } + } else { // max + _max = data.as(); + if (_state && _value >= _max) { + _state = false; + JsonVariant value(_value); + processOutput(value, 0); + } + } + } +}; + +// ----------------------------------------------------------------------------- +// Terminal component +// ----------------------------------------------------------------------------- + +#if TERMINAL_SUPPORT + +PROGMEM const char flow_run[] = "Run"; +PROGMEM const char flow_command[] = "Command"; +PROGMEM const char* const flow_terminal_inputs[] = {flow_run, flow_command}; + +PROGMEM const FlowConnections flow_terminal_component = { + 2, flow_terminal_inputs, + 0, NULL, +}; + +class FlowTerminalComponent : public FlowComponent { + private: + String _command; + + public: + FlowTerminalComponent(JsonObject& properties) { + const char * command = properties["Command"]; + _command = String(command != NULL ? command : ""); + } + + virtual void processInput(JsonVariant& data, int inputNumber) { + if (inputNumber == 0) { + char buffer[_command.length() + 2]; + snprintf(buffer, sizeof(buffer), "%s\n", _command.c_str()); + terminalInject((void*) buffer, strlen(buffer)); + } else if (inputNumber == 1) { + _command = toString(data); + } + } + + static void reg() { + flowRegisterComponent("Terminal", &flow_terminal_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowTerminalComponent(properties); })); + } +}; + +#endif // TERMINAL_SUPPORT + +#if !SPIFFS_SUPPORT && MQTT_SUPPORT +void _flowMQTTCallback(unsigned int type, const char * topic, const char * payload) { + + if (type == MQTT_CONNECT_EVENT) { + mqttSubscribe(FLOW_MQTT_TOPIC); + } + + if (type == MQTT_MESSAGE_EVENT) { + // Match topic + String t = mqttMagnitude((char *) topic); + if (t.equals(FLOW_MQTT_TOPIC) && millis() - _mqtt_flow_sent_at > MQTT_SKIP_TIME) { + _flow = String(payload); + flowStart(); + } + } +} +#endif + +void flowSetup() { + #if !SPIFFS_SUPPORT && MQTT_SUPPORT + mqttRegister(_flowMQTTCallback); + #endif + + flowRegisterComponent("Start", &flow_start_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowStartComponent(properties); })); + + flowRegisterComponent("Debug", &flow_debug_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowDebugComponent(properties); })); + + flowRegisterComponent("Change", &flow_change_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowChangeComponent(properties); })); + + FlowMathComponent::reg(); + FlowCompareComponent::reg(); + + flowRegisterComponent("Delay", &flow_delay_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowDelayComponent(properties); })); + + flowRegisterComponent("Timer", &flow_timer_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowTimerComponent(properties); })); + + flowRegisterComponent("Gate", &flow_gate_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowGateComponent(properties); })); + + flowRegisterComponent("Hysteresis", &flow_hysteresis_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowHysteresisComponent(properties); })); + + #if TERMINAL_SUPPORT + FlowTerminalComponent::reg(); + #endif + + #if SPIFFS_SUPPORT + flowStart(); + #endif +} + +#endif // FLOW_SUPPORT diff --git a/code/espurna/light.ino b/code/espurna/light.ino index d3db6adfad..5c36d3e459 100644 --- a/code/espurna/light.ino +++ b/code/espurna/light.ino @@ -647,6 +647,44 @@ void lightBroker() { #endif +// ----------------------------------------------------------------------------- +// FLOW +// ----------------------------------------------------------------------------- + +#if FLOW_SUPPORT + +PROGMEM const char flow_color[] = "Color"; +PROGMEM const char flow_brightness[] = "Brightness"; +PROGMEM const char* const flow_light_component_inputs[] = {flow_color, flow_brightness}; + +PROGMEM const FlowConnections flow_light_component = { + 2, flow_light_component_inputs, + 0, NULL, +}; + +class FlowLightComponent : public FlowComponent { + public: + FlowLightComponent(JsonObject& properties) { + } + + virtual void processInput(JsonVariant& data, int inputNumber) { + if (inputNumber == 0) { // color + lightColor(data.as(), true); + lightUpdate(true, true); + } else { // brightness + _light_brightness = constrain(data.as(), 0, LIGHT_MAX_BRIGHTNESS); + lightUpdate(true, true); + } + } + + static void reg() { + flowRegisterComponent("Light", &flow_light_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowLightComponent(properties); })); + } +}; + +#endif // FLOW_SUPPORT + // ----------------------------------------------------------------------------- // API // ----------------------------------------------------------------------------- @@ -1186,6 +1224,10 @@ void lightSetup() { _lightInitCommands(); #endif + #if FLOW_SUPPORT + FlowLightComponent::reg(); + #endif + // Main callbacks espurnaRegisterReload([]() { #if LIGHT_SAVE_ENABLED == 0 diff --git a/code/espurna/mqtt.ino b/code/espurna/mqtt.ino index 066417bf2f..b27c1dd26a 100644 --- a/code/espurna/mqtt.ino +++ b/code/espurna/mqtt.ino @@ -360,6 +360,76 @@ void _mqttInitCommands() { #endif // TERMINAL_SUPPORT +// ----------------------------------------------------------------------------- +// FLOW +// ----------------------------------------------------------------------------- + +#if FLOW_SUPPORT + +PROGMEM const FlowConnections flow_mqtt_subscribe_component = { + 0, NULL, + 1, flow_data_array, +}; + +class FlowMqttSubscribeComponent : public FlowComponent { + private: + String _topic; + + void mqttCallback(unsigned int type, const char * topic, const char * payload) { + if (type == MQTT_CONNECT_EVENT) { + mqttSubscribeRaw(_topic.c_str()); + } + + if (type == MQTT_MESSAGE_EVENT) { + if (strcmp(topic, _topic.c_str()) == 0) { + JsonVariant data(payload); + processOutput(data, 0); + } + } + } + + public: + FlowMqttSubscribeComponent(JsonObject& properties) { + const char * topic = properties["Topic"]; + _topic = String(topic != NULL ? topic : ""); + _mqttPlaceholders(&_topic); + + mqtt_callback_f callback = [this](unsigned int type, const char * topic, const char * payload){ this->mqttCallback(type, topic, payload); }; + mqttRegister(callback); + + // emulate connect event if MQTT is connected already + if (mqttConnected) { + mqttCallback(MQTT_CONNECT_EVENT, NULL, NULL); + } + } +}; + +PROGMEM const FlowConnections flow_mqtt_publish_component = { + 1, flow_data_array, + 0, NULL, +}; + +class FlowMqttPublishComponent : public FlowComponent { + private: + String _topic; + bool _retain; + + public: + FlowMqttPublishComponent(JsonObject& properties) { + const char * topic = properties["Topic"]; + _topic = String(topic != NULL ? topic : ""); + _mqttPlaceholders(&_topic); + + _retain = properties["Retain"]; + } + + virtual void processInput(JsonVariant& data, int inputNumber) { + String s = toString(data); + mqttSendRaw(_topic.c_str(), s.c_str(), _retain); + } +}; +#endif //FLOW_SUPPORT + // ----------------------------------------------------------------------------- // MQTT Callbacks // ----------------------------------------------------------------------------- @@ -432,7 +502,15 @@ void _mqttOnMessage(char* topic, char* payload, unsigned int len) { strlcpy(message, (char *) payload, len + 1); #if MQTT_SKIP_RETAINED - if (millis() - _mqtt_last_connection < MQTT_SKIP_TIME) { + bool skip = millis() - _mqtt_last_connection < MQTT_SKIP_TIME; + + #if FLOW_SUPPORT + // workaround for persisted flow + if (skip && mqttMagnitude((char *) topic).equals(FLOW_MQTT_TOPIC)) + skip = false; + #endif + + if (skip) { DEBUG_MSG_P(PSTR("[MQTT] Received %s => %s - SKIPPED\n"), topic, message); return; } @@ -836,6 +914,14 @@ void mqttSetup() { _mqttInitCommands(); #endif + #if FLOW_SUPPORT + flowRegisterComponent("MQTT subscribe", &flow_mqtt_subscribe_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowMqttSubscribeComponent(properties); })); + + flowRegisterComponent("MQTT publish", &flow_mqtt_publish_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowMqttPublishComponent(properties); })); + #endif + // Main callbacks espurnaRegisterLoop(mqttLoop); espurnaRegisterReload(_mqttConfigure); diff --git a/code/espurna/relay.ino b/code/espurna/relay.ino index 2294a1226e..f2e5c0590d 100644 --- a/code/espurna/relay.ino +++ b/code/espurna/relay.ino @@ -1057,6 +1057,41 @@ void _relayInitCommands() { #endif // TERMINAL_SUPPORT +// ----------------------------------------------------------------------------- +// FLOW +// ----------------------------------------------------------------------------- + +#if FLOW_SUPPORT + +PROGMEM const char flow_toggle[] = "Toggle"; +PROGMEM const char* const flow_relay_component_inputs[] = {flow_state, flow_toggle}; + +PROGMEM const FlowConnections flow_relay_component = { + 2, flow_relay_component_inputs, + 0, NULL, +}; + +class FlowRelayComponent : public FlowComponent { + private: + int _relay_id; + public: + FlowRelayComponent(JsonObject& properties) { + _relay_id = properties["Relay"]; + } + + virtual void processInput(JsonVariant& data, int inputNumber) { + if (inputNumber == 0) { // State + bool state = data.as(); + relayStatus(_relay_id, state); + } else { // Toggle + relayToggle(_relay_id); + } + } + +}; + +#endif // FLOW_SUPPORT + //------------------------------------------------------------------------------ // Setup //------------------------------------------------------------------------------ @@ -1118,6 +1153,17 @@ void relaySetup() { #if TERMINAL_SUPPORT _relayInitCommands(); #endif + #if FLOW_SUPPORT + std::vector* relays = new std::vector(); + for (unsigned int i=0; i < _relays.size(); i++) { + relays->push_back(String(i)); + } + + flowRegisterComponent("Relay", &flow_relay_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowRelayComponent(properties); })); + flowRegisterComponentValues("RELAY_VALUES", relays); + #endif + // Main callbacks espurnaRegisterLoop(_relayLoop); diff --git a/code/espurna/scheduler.ino b/code/espurna/scheduler.ino index 7e2debfc9f..e21dd20537 100644 --- a/code/espurna/scheduler.ino +++ b/code/espurna/scheduler.ino @@ -60,6 +60,52 @@ void _schWebSocketOnSend(JsonObject &root){ #endif // WEB_SUPPORT +// ----------------------------------------------------------------------------- +// FLOW +// ----------------------------------------------------------------------------- + +#if FLOW_SUPPORT + +PROGMEM const FlowConnections flow_schedule_component = { + 0, NULL, + 1, flow_data_array, +}; + +class FlowScheduleComponent; +std::vector _schedule_components; + +class FlowScheduleComponent : public FlowComponent { + private: + JsonVariant *_value; + String _weekdays; + int _hour; + int _minute; + + public: + FlowScheduleComponent(JsonObject& properties) { + JsonVariant value = properties["Value"]; + _value = clone(value); + + _weekdays = String((const char *)properties["Weekdays"]); + String time = String((const char *)properties["Time"]); + int colon = time.indexOf(":"); + if (colon > 0) { + _hour = time.substring(0, colon).toInt(); + _minute = time.substring(colon + 1).toInt(); + } + + _schedule_components.push_back(this); + } + + void check(time_t& time) { + if (_schMinutesLeft(time, _hour, _minute) == 0 && (_weekdays.length() == 0 || _schIsThisWeekday(time, _weekdays))) { + processOutput(*_value, 0); + } + } +}; + +#endif // FLOW_SUPPORT + // ----------------------------------------------------------------------------- void _schConfigure() { @@ -204,6 +250,11 @@ void _schCheck() { } + #if FLOW_SUPPORT + for (unsigned int i=0; i < _schedule_components.size(); i++) { + _schedule_components[i]->check(local_time); + } + #endif } void _schLoop() { @@ -233,6 +284,11 @@ void schSetup() { wsOnReceiveRegister(_schWebSocketOnReceive); #endif + #if FLOW_SUPPORT + flowRegisterComponent("Schedule", &flow_schedule_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowScheduleComponent(properties); })); + #endif + // Main callbacks espurnaRegisterLoop(_schLoop); espurnaRegisterReload(_schConfigure); diff --git a/code/espurna/sensor.ino b/code/espurna/sensor.ino index 819db4d122..19dfe7222d 100644 --- a/code/espurna/sensor.ino +++ b/code/espurna/sensor.ino @@ -384,6 +384,56 @@ void _sensorInitCommands() { #endif +#if FLOW_SUPPORT + +PROGMEM const FlowConnections flow_sensor_component = { + 0, NULL, + 1, flow_data_array, +}; + +class FlowSensorComponent; +std::vector _flow_sensors; + +class FlowSensorComponent : public FlowComponent { + private: + int _magnitude = -1; + + public: + FlowSensorComponent(JsonObject& properties) { + String magnitude = properties["Sensor"]; + + int slash = magnitude.indexOf("/"); + if (slash < 0) { + DEBUG_MSG_P("[FLOW] Sensor %s has incorrect name\n", magnitude.c_str()); + return; + } + String sensor = magnitude.substring(0, slash); + String topic = magnitude.substring(slash + 1); + + for (unsigned char i = 0; i < _magnitudes.size(); i++) { + sensor_magnitude_t m = _magnitudes[i]; + if (magnitudeName(i).equals(sensor) && magnitudeTopic(m.type).equals(topic)) { + _magnitude = i; + } + } + + if (_magnitude >= 0) { + _flow_sensors.push_back(this); + } else { + DEBUG_MSG_P("[FLOW] Sensor %s not found\n", magnitude.c_str()); + } + } + + void sensorReport(int magnitude, double value) { + if (magnitude == _magnitude) { + JsonVariant data(value); + processOutput(data, 0); + } + } +}; + +#endif + void _sensorTick() { for (unsigned char i=0; i<_sensors.size(); i++) { _sensors[i]->tick(); @@ -1275,6 +1325,12 @@ void _sensorReport(unsigned char index, double value) { } #endif // DOMOTICZ_SUPPORT + #if FLOW_SUPPORT + for (FlowSensorComponent* component : _flow_sensors) { + component->sensorReport(index, value); + } + #endif + } // ----------------------------------------------------------------------------- @@ -1390,6 +1446,21 @@ void sensorSetup() { _sensorInitCommands(); #endif + // Flow + #if FLOW_SUPPORT + std::vector* sensors = new std::vector(); + for (unsigned char i = 0; i < _magnitudes.size(); i++) { + sensor_magnitude_t magnitude = _magnitudes[i]; + String sensor = magnitudeName(i); + String topic = magnitudeTopic(magnitude.type); + sensors->push_back(sensor + "/" + topic); + } + + flowRegisterComponent("Sensor", &flow_sensor_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowSensorComponent(properties); })); + flowRegisterComponentValues("SENSOR_VALUES", sensors); + #endif + // Main callbacks espurnaRegisterLoop(sensorLoop); espurnaRegisterReload(_sensorConfigure); diff --git a/code/espurna/settings.ino b/code/espurna/settings.ino index 8d323d0217..f8e2a71d78 100644 --- a/code/espurna/settings.ino +++ b/code/espurna/settings.ino @@ -211,6 +211,79 @@ void settingsGetJson(JsonObject& root) { } +// ----------------------------------------------------------------------------- +// FLOW +// ----------------------------------------------------------------------------- + +#if FLOW_SUPPORT + +//PROGMEM const char flow_value[] = "Value"; +PROGMEM const char* const flow_value_array[] = {flow_value}; + +PROGMEM const FlowConnections flow_save_setting_component = { + 1, flow_value_array, + 0, NULL, +}; + +class FlowSaveSettingComponent : public FlowComponent { + private: + String _name; + + public: + FlowSaveSettingComponent(JsonObject& properties) { + const char * name = properties["Name"]; + _name = String(name != NULL ? name : ""); + } + + virtual void processInput(JsonVariant& data, int inputNumber) { + String value = data.as(); + if (value != "") { + setSetting(_name, value); + } else { + delSetting(_name); + } + } + + static void reg() { + flowRegisterComponent("Save setting", &flow_save_setting_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowSaveSettingComponent(properties); })); + } +}; + +PROGMEM const char flow_name[] = "Name"; +PROGMEM const char* const flow_name_array[] = {flow_name}; + +PROGMEM const FlowConnections flow_load_setting_component = { + 1, flow_name_array, + 1, flow_value_array, +}; + +class FlowLoadSettingComponent : public FlowComponent { + private: + String _default; + + public: + FlowLoadSettingComponent(JsonObject& properties) { + const char * def = properties["Default"]; + _default = String(def != NULL ? def : ""); + } + + virtual void processInput(JsonVariant& data, int inputNumber) { + String name = data.as(); + String value = getSetting(name, _default); + JsonVariant output(value.c_str()); + processOutput(output, 0); + } + + static void reg() { + flowRegisterComponent("Load setting", &flow_load_setting_component, + (flow_component_factory_f)([] (JsonObject& properties) { return new FlowLoadSettingComponent(properties); })); + } +}; + + +#endif // FLOW_SUPPORT + // ----------------------------------------------------------------------------- // Initialization // ----------------------------------------------------------------------------- @@ -228,4 +301,9 @@ void settingsSetup() { #endif ); + #if FLOW_SUPPORT + FlowSaveSettingComponent::reg(); + FlowLoadSettingComponent::reg(); + #endif + } \ No newline at end of file diff --git a/code/espurna/web.ino b/code/espurna/web.ino index 7c4b6f1522..299a028e8f 100644 --- a/code/espurna/web.ino +++ b/code/espurna/web.ino @@ -359,6 +359,83 @@ void _onBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t i } +#if FLOW_SUPPORT +std::vector * _webFlowBuffer; +bool _webFlowSuccess = false; + +void _onGetFlowLibrary(AsyncWebServerRequest *request) { + + webLog(request); + if (!webAuthenticate(request)) { + return request->requestAuthentication(getSetting("hostname").c_str()); + } + + AsyncWebServerResponse *response = request->beginResponse_P(200, "text/json", flow_library_json, flowLibraryProcessor); + + response->addHeader("Content-Disposition", "inline; filename=\"library.json\""); + response->addHeader("X-XSS-Protection", "1; mode=block"); + response->addHeader("X-Content-Type-Options", "nosniff"); + response->addHeader("X-Frame-Options", "deny"); + + request->send(response); +} + +void _onGetFlowConfig(AsyncWebServerRequest *request) { + + webLog(request); + if (!webAuthenticate(request)) { + return request->requestAuthentication(getSetting("hostname").c_str()); + } + + AsyncWebServerResponse *response = flowGetConfigResponse(request); + + response->addHeader("X-XSS-Protection", "1; mode=block"); + response->addHeader("X-Content-Type-Options", "nosniff"); + response->addHeader("X-Frame-Options", "deny"); + + request->send(response); +} + +void _onPostFlowConfig(AsyncWebServerRequest *request) { + webLog(request); + if (!webAuthenticate(request)) { + return request->requestAuthentication(getSetting("hostname").c_str()); + } + request->send(_webFlowSuccess ? 200 : 400); +} + +void _onPostFlowConfigData(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + // No buffer + if (final && (index == 0)) { + data[len] = 0; + _webFlowSuccess = flowSaveConfig((char *) data); + return; + } + + // Buffer start => reset + if (index == 0) if (_webFlowBuffer) delete _webFlowBuffer; + + // init buffer if it doesn't exist + if (!_webFlowBuffer) { + _webFlowBuffer = new std::vector(); + _webFlowSuccess = false; + } + + // Copy + if (len > 0) { + _webFlowBuffer->reserve(_webFlowBuffer->size() + len); + _webFlowBuffer->insert(_webFlowBuffer->end(), data, data + len); + } + + // Ending + if (final) { + _webFlowBuffer->push_back(0); + _webFlowSuccess = flowSaveConfig((char *) _webFlowBuffer->data()); + delete _webFlowBuffer; + } +} +#endif + // ----------------------------------------------------------------------------- @@ -423,6 +500,12 @@ void webSetup() { _server->on("/upgrade", HTTP_POST, _onUpgrade, _onUpgradeData); _server->on("/discover", HTTP_GET, _onDiscover); + #if FLOW_SUPPORT + _server->on("/flow_library", HTTP_GET, _onGetFlowLibrary); + _server->on("/flow", HTTP_GET, _onGetFlowConfig); + _server->on("/flow", HTTP_POST | HTTP_PUT, _onPostFlowConfig, _onPostFlowConfigData); + #endif + // Serve static files #if SPIFFS_SUPPORT _server->serveStatic("/", SPIFFS, "/")