From eb59726b4fb128ec99485ba3dfed7385145f189e Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Sun, 24 Oct 2021 14:10:14 +0300 Subject: [PATCH] sns: report both used & supported units to the webui Generic way to find out which units the magnitude type supports. Clean-up UI related to temperature, energy and power units. Resolves #2482 Also apply some refactoring to the 'schema'-generated payloads by using the EnumerableConfig helper class. Class received some new features: - optional callback, verifying that index should be used at all specific use-case is magnitudes list that needs only 'counted' ones - std::iota / ranges::iota_view -like helper object for sequences starting with something other than 0 Reordered payloads, prefer queueing over sending everything at once. --- code/espurna/domoticz.cpp | 4 +- code/espurna/sensor.cpp | 721 +++++++++++++++++++++++------------- code/espurna/sensor.h | 3 +- code/espurna/thingspeak.cpp | 4 +- code/espurna/ws.cpp | 18 +- code/espurna/ws_utils.h | 91 ++++- code/html/custom.css | 1 - code/html/custom.js | 106 ++++-- code/html/index.html | 75 ++-- 9 files changed, 662 insertions(+), 361 deletions(-) diff --git a/code/espurna/domoticz.cpp b/code/espurna/domoticz.cpp index 7087345d..b8a6f41b 100644 --- a/code/espurna/domoticz.cpp +++ b/code/espurna/domoticz.cpp @@ -409,7 +409,9 @@ void onConnected(JsonObject& root) { } #if SENSOR_SUPPORT - sensorWebSocketMagnitudes(root, "dcz"); + sensorWebSocketMagnitudes(root, "dcz", [](JsonArray& out, size_t index) { + out.add(getSetting({"dczMagnitude", index}, "0")); + }); #endif } diff --git a/code/espurna/sensor.cpp b/code/espurna/sensor.cpp index 8e34b30b..ffe0b74a 100644 --- a/code/espurna/sensor.cpp +++ b/code/espurna/sensor.cpp @@ -779,152 +779,326 @@ String _magnitudeTopic(unsigned char type) { } -String _magnitudeUnits(const sensor_magnitude_t& magnitude) { +String _magnitudeUnits(sensor::Unit unit) { + const __FlashStringHelper* result { F("") }; - const __FlashStringHelper* result = nullptr; - - switch (magnitude.units) { - case sensor::Unit::Farenheit: - result = F("°F"); - break; - case sensor::Unit::Celcius: - result = F("°C"); - break; - case sensor::Unit::Percentage: - result = F("%"); - break; - case sensor::Unit::Hectopascal: - result = F("hPa"); - break; - case sensor::Unit::Ampere: - result = F("A"); - break; - case sensor::Unit::Volt: - result = F("V"); - break; - case sensor::Unit::Watt: - result = F("W"); - break; - case sensor::Unit::Kilowatt: - result = F("kW"); - break; - case sensor::Unit::Voltampere: - result = F("VA"); - break; - case sensor::Unit::Kilovoltampere: - result = F("kVA"); - break; - case sensor::Unit::VoltampereReactive: - result = F("VAR"); - break; - case sensor::Unit::KilovoltampereReactive: - result = F("kVAR"); - break; - case sensor::Unit::Joule: - //aka case sensor::Unit::WattSecond: - result = F("J"); - break; - case sensor::Unit::KilowattHour: - result = F("kWh"); - break; - case sensor::Unit::MicrogrammPerCubicMeter: - result = F("µg/m³"); - break; - case sensor::Unit::PartsPerMillion: - result = F("ppm"); - break; - case sensor::Unit::Lux: - result = F("lux"); - break; - case sensor::Unit::Ohm: - result = F("ohm"); - break; - case sensor::Unit::MilligrammPerCubicMeter: - result = F("mg/m³"); - break; - case sensor::Unit::CountsPerMinute: - result = F("cpm"); - break; - case sensor::Unit::MicrosievertPerHour: - result = F("µSv/h"); - break; - case sensor::Unit::Meter: - result = F("m"); - break; - case sensor::Unit::Hertz: - result = F("Hz"); - break; - case sensor::Unit::None: - default: - result = F(""); - break; + switch (unit) { + case sensor::Unit::Farenheit: + result = F("°F"); + break; + case sensor::Unit::Celcius: + result = F("°C"); + break; + case sensor::Unit::Kelvin: + result = F("K"); + break; + case sensor::Unit::Percentage: + result = F("%"); + break; + case sensor::Unit::Hectopascal: + result = F("hPa"); + break; + case sensor::Unit::Ampere: + result = F("A"); + break; + case sensor::Unit::Volt: + result = F("V"); + break; + case sensor::Unit::Watt: + result = F("W"); + break; + case sensor::Unit::Kilowatt: + result = F("kW"); + break; + case sensor::Unit::Voltampere: + result = F("VA"); + break; + case sensor::Unit::Kilovoltampere: + result = F("kVA"); + break; + case sensor::Unit::VoltampereReactive: + result = F("VAR"); + break; + case sensor::Unit::KilovoltampereReactive: + result = F("kVAR"); + break; + case sensor::Unit::Joule: + //aka case sensor::Unit::WattSecond: + result = F("J"); + break; + case sensor::Unit::KilowattHour: + result = F("kWh"); + break; + case sensor::Unit::MicrogrammPerCubicMeter: + result = F("µg/m³"); + break; + case sensor::Unit::PartsPerMillion: + result = F("ppm"); + break; + case sensor::Unit::Lux: + result = F("lux"); + break; + case sensor::Unit::UltravioletIndex: + break; + case sensor::Unit::Ohm: + result = F("ohm"); + break; + case sensor::Unit::MilligrammPerCubicMeter: + result = F("mg/m³"); + break; + case sensor::Unit::CountsPerMinute: + result = F("cpm"); + break; + case sensor::Unit::MicrosievertPerHour: + result = F("µSv/h"); + break; + case sensor::Unit::Meter: + result = F("m"); + break; + case sensor::Unit::Hertz: + result = F("Hz"); + break; + case sensor::Unit::Ph: + result = F("pH"); + break; + case sensor::Unit::Min_: + case sensor::Unit::Max_: + case sensor::Unit::None: + break; } return String(result); +} +String _magnitudeUnits(const sensor_magnitude_t& magnitude) { + return _magnitudeUnits(magnitude.units); } } // namespace String magnitudeUnits(unsigned char index) { - if (index >= magnitudeCount()) return String(); - return _magnitudeUnits(_magnitudes[index]); + if (index < _magnitudes.size()) { + return _magnitudeUnits(_magnitudes[index]); + } + + return String(); } namespace { // Choose unit based on type of magnitude we use -sensor::Unit _magnitudeUnitFilter(const sensor_magnitude_t& magnitude, sensor::Unit updated) { - auto result = magnitude.units; +struct MagnitudeUnitsRange { + MagnitudeUnitsRange() = default; - switch (magnitude.type) { + template + explicit MagnitudeUnitsRange(const sensor::Unit (&units)[Size]) : + _begin(std::begin(units)), + _end(std::end(units)) + {} + + template + MagnitudeUnitsRange& operator=(const sensor::Unit (&units)[Size]) { + _begin = std::begin(units); + _end = std::end(units); + return *this; + } + + const sensor::Unit* begin() const { + return _begin; + } + + const sensor::Unit* end() const { + return _end; + } + +private: + const sensor::Unit* _begin { nullptr }; + const sensor::Unit* _end { nullptr }; +}; + +#define MAGNITUDE_UNITS_RANGE(...)\ + static const sensor::Unit units[] PROGMEM {\ + __VA_ARGS__\ + };\ +\ + out = units + +MagnitudeUnitsRange _magnitudeUnitsRange(unsigned char type) { + MagnitudeUnitsRange out; + + switch (type) { case MAGNITUDE_TEMPERATURE: { - switch (updated) { - case sensor::Unit::Celcius: - case sensor::Unit::Farenheit: - case sensor::Unit::Kelvin: - result = updated; - break; - default: - break; - } + MAGNITUDE_UNITS_RANGE( + sensor::Unit::Celcius, + sensor::Unit::Farenheit, + sensor::Unit::Kelvin + ); + break; + } + + case MAGNITUDE_HUMIDITY: + case MAGNITUDE_POWER_FACTOR: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::Percentage + ); + break; + } + + case MAGNITUDE_PRESSURE: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::Hectopascal + ); + break; + } + + case MAGNITUDE_CURRENT: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::Ampere + ); + break; + } + + case MAGNITUDE_VOLTAGE: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::Volt + ); break; } case MAGNITUDE_POWER_ACTIVE: { - switch (updated) { - case sensor::Unit::Kilowatt: - case sensor::Unit::Watt: - result = updated; - break; - default: - break; - } + MAGNITUDE_UNITS_RANGE( + sensor::Unit::Kilowatt, + sensor::Unit::Watt + ); + break; + } + + case MAGNITUDE_POWER_APPARENT: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::Voltampere, + sensor::Unit::Kilovoltampere + ); + break; + } + + case MAGNITUDE_POWER_REACTIVE: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::VoltampereReactive, + sensor::Unit::KilovoltampereReactive + ); + break; + } + + case MAGNITUDE_ENERGY_DELTA: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::Joule + ); break; } case MAGNITUDE_ENERGY: { - switch (updated) { - case sensor::Unit::KilowattHour: - case sensor::Unit::Joule: - result = updated; - break; - default: - break; - } - break; - } - - default: - result = updated; + MAGNITUDE_UNITS_RANGE( + sensor::Unit::KilowattHour, + sensor::Unit::Joule + ); break; + } + + case MAGNITUDE_PM1dot0: + case MAGNITUDE_PM2dot5: + case MAGNITUDE_PM10: + case MAGNITUDE_TVOC: + case MAGNITUDE_CH2O: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::MicrogrammPerCubicMeter, + sensor::Unit::MilligrammPerCubicMeter + ); + break; + } + + case MAGNITUDE_CO: + case MAGNITUDE_CO2: + case MAGNITUDE_NO2: + case MAGNITUDE_VOC: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::PartsPerMillion + ); + break; + } + + case MAGNITUDE_LUX: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::Lux + ); + break; + } + + case MAGNITUDE_RESISTANCE: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::Ohm + ); + break; + } + + case MAGNITUDE_HCHO: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::MilligrammPerCubicMeter + ); + break; + } + + case MAGNITUDE_GEIGER_CPM: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::CountsPerMinute + ); + break; + } + + case MAGNITUDE_GEIGER_SIEVERT: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::MicrosievertPerHour + ); + break; + } + + case MAGNITUDE_DISTANCE: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::Meter + ); + break; + } + + case MAGNITUDE_FREQUENCY: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::Hertz + ); + break; + } + + case MAGNITUDE_PH: { + MAGNITUDE_UNITS_RANGE( + sensor::Unit::Ph + ); + break; + } } - return result; -}; + return out; +} + +bool _magnitudeUnitSupported(const sensor_magnitude_t& magnitude, sensor::Unit unit) { + const auto range = _magnitudeUnitsRange(magnitude.type); + return std::any_of(range.begin(), range.end(), [&](sensor::Unit supported) { + return (unit == supported); + }); +} + +sensor::Unit _magnitudeUnitFilter(const sensor_magnitude_t& magnitude, sensor::Unit unit) { + return _magnitudeUnitSupported(magnitude, unit) ? unit : magnitude.units; +} double _magnitudeProcess(const sensor_magnitude_t& magnitude, double value) { @@ -1310,108 +1484,111 @@ String _magnitudeName(unsigned char type) { return String(result); } -// prepare available magnitude, unit and error types +// prepare available types and magnitudes config +// make sure these are properly ordered, as UI does not delay processing void _sensorWebSocketTypes(JsonObject& root) { - JsonObject& container = root.createNestedObject("types"); - static const char* const keys[] PROGMEM = { - "type", "prefix", "name" - }; - - JsonArray& schema = container.createNestedArray("schema"); - schema.copyFrom(keys, sizeof(keys) / sizeof(*keys)); - - JsonArray& values = container.createNestedArray("values"); - _magnitudeForEachCounted([&](unsigned char type) { - JsonArray& value = values.createNestedArray(); - value.add(type); - value.add(_magnitudeSettingsPrefix(type)); - value.add(_magnitudeName(type)); - }); + ::web::ws::EnumerableConfig config{root, F("types")}; + config(F("values"), {MAGNITUDE_NONE + 1, MAGNITUDE_MAX}, + [](size_t type) { + return sensor_magnitude_t::counts(type) > 0; + }, + { + {F("type"), [](JsonArray& out, size_t index) { + out.add(index); + }}, + {F("prefix"), [](JsonArray& out, size_t index) { + out.add(_magnitudeSettingsPrefix(index)); + }}, + {F("name"), [](JsonArray& out, size_t index) { + out.add(_magnitudeName(index)); + }} + }); } void _sensorWebSocketErrors(JsonObject& root) { - JsonObject& container = root.createNestedObject("errors"); - static const char* const keys[] PROGMEM = { - "type", "name" - }; - - JsonArray& schema = container.createNestedArray("schema"); - schema.copyFrom(keys, sizeof(keys) / sizeof(*keys)); - - JsonArray& values = container.createNestedArray("values"); - _sensorForEachError([&](unsigned char type) { - JsonArray& value = values.createNestedArray(); - value.add(type); - value.add(_sensorError(type)); + ::web::ws::EnumerableConfig config{root, F("errors")}; + config(F("values"), SENSOR_ERROR_MAX, { + {F("type"), [](JsonArray& out, size_t index) { + out.add(index); + }}, + {F("name"), [](JsonArray& out, size_t index) { + out.add(_sensorError(index)); + }} }); } -void _sensorWebSocketMagnitudes(JsonObject& root) { - JsonObject& container = root.createNestedObject("magnitudes"); - static const char* const keys[] PROGMEM = { - "index_global", "type", "units", "description" - }; - - JsonArray& schema = container.createNestedArray("schema"); - schema.copyFrom(keys, sizeof(keys) / sizeof(*keys)); - - JsonArray& values = container.createNestedArray("values"); - for (auto& magnitude : _magnitudes) { - JsonArray& value = values.createNestedArray(); - value.add(magnitude.index_global); - value.add(magnitude.type); - value.add(_magnitudeUnits(magnitude)); - value.add(_magnitudeDescription(magnitude)); - } +void _sensorWebSocketUnits(JsonObject& root) { + ::web::ws::EnumerableConfig config{root, F("units")}; + config(F("values"), _magnitudes.size(), { + {F("type"), [](JsonArray& out, size_t index) { + out.add(_magnitudes[index].type); + }}, + {F("index_global"), [](JsonArray& out, size_t index) { + out.add(_magnitudes[index].index_global); + }}, + {F("supported"), [](JsonArray& out, size_t index) { + JsonArray& units = out.createNestedArray(); + const auto range = _magnitudeUnitsRange(_magnitudes[index].type); + for (auto it = range.begin(); it != range.end(); ++it) { + JsonArray& unit = units.createNestedArray(); + unit.add(static_cast(*it)); + unit.add(_magnitudeUnits(*it)); + } + }} + }); } -void _sensorWebSocketMagnitudesConfig(JsonObject& root) { - JsonObject& container = root.createNestedObject("magnitudesConfig"); - _sensorWebSocketTypes(container); - _sensorWebSocketErrors(container); - _sensorWebSocketMagnitudes(container); +void _sensorWebSocketConfig(JsonObject& root) { + ::web::ws::EnumerableConfig config{root, F("magnitudes")}; + config(F("values"), _magnitudes.size(), { + {F("index_global"), [](JsonArray& out, size_t index) { + out.add(_magnitudes[index].index_global); + }}, + {F("type"), [](JsonArray& out, size_t index) { + out.add(_magnitudes[index].type); + }}, + {F("description"), [](JsonArray& out, size_t index) { + out.add(_magnitudeDescription(_magnitudes[index])); + }}, + {F("units"), [](JsonArray& out, size_t index) { + out.add(static_cast(_magnitudes[index].units)); + }} + }); } void _sensorWebSocketSendData(JsonObject& root) { - JsonObject& container = root.createNestedObject("magnitudes"); - static const char* const keys[] PROGMEM = { - "value", "error", "info" - }; - - JsonArray& schema = container.createNestedArray("schema"); - schema.copyFrom(keys, sizeof(keys) / sizeof(*keys)); - - char buffer[64]; - JsonArray& values = container.createNestedArray("values"); - for (auto& magnitude : _magnitudes) { - JsonArray& entry = values.createNestedArray(); - dtostrf(_magnitudeProcess(magnitude, magnitude.last), 1, magnitude.decimals, buffer); - - entry.add(buffer); - entry.add(magnitude.sensor->error()); - + ::web::ws::EnumerableConfig config{root, F("magnitudes")}; + config(F("values"), _magnitudes.size(), { + {F("value"), [](JsonArray& out, size_t index) { + char buffer[64]; + dtostrf(_magnitudeProcess( + _magnitudes[index], _magnitudes[index].last), + 1, _magnitudes[index].decimals, buffer); + out.add(buffer); + }}, + {F("error"), [](JsonArray& out, size_t index) { + out.add(_magnitudes[index].sensor->error()); + }}, + {F("info"), [](JsonArray& out, size_t index) { #if NTP_SUPPORT - if ((_sensor_save_every > 0) && (magnitude.type == MAGNITUDE_ENERGY)) { - String string = F("Last saved: "); - string += getSetting({"eneTime", magnitude.index_global}, F("(unknown)")); - entry.add(string); - } else { - entry.add(""); - } -#else - entry.add(""); + if ((_magnitudes[index].type == MAGNITUDE_ENERGY) && (_sensor_save_every > 0)) { + out.add(String(F("Last saved: ")) + + getSetting({"eneTime", _magnitudes[index].index_global}, + F("(unknown)"))); + } else { #endif - } + out.add(""); +#if NTP_SUPPORT + } +#endif + }} + }); } void _sensorWebSocketOnVisible(JsonObject& root) { wsPayloadModule(root, "sns"); -} - -void _sensorWebSocketOnConnected(JsonObject& root) { for (auto* sensor [[gnu::unused]] : _sensors) { - if (_sensorIsEmon(sensor)) { wsPayloadModule(root, "emon"); wsPayloadModule(root, "pwr"); @@ -1421,85 +1598,96 @@ void _sensorWebSocketOnConnected(JsonObject& root) { root["voltMains0"] = static_cast(sensor)->getVoltage(); } - #if HLW8012_SUPPORT - if (sensor->getID() == SENSOR_HLW8012_ID) { - wsPayloadModule(root, "hlw"); - } - #endif - - #if CSE7766_SUPPORT - if (sensor->getID() == SENSOR_CSE7766_ID) { - wsPayloadModule(root, "cse"); - } - #endif - - #if PZEM004T_SUPPORT || PZEM004TV30_SUPPORT - switch (sensor->getID()) { - case SENSOR_PZEM004T_ID: - case SENSOR_PZEM004TV30_ID: - wsPayloadModule(root, "pzem"); - break; - default: - break; - } - #endif - - #if PULSEMETER_SUPPORT - if (sensor->getID() == SENSOR_PULSEMETER_ID) { - wsPayloadModule(root, "pm"); - root["eneRatio0"] = ((PulseMeterSensor *) sensor)->getEnergyRatio(); - } - #endif - - #if MICS2710_SUPPORT || MICS5525_SUPPORT - switch (sensor->getID()) { - case SENSOR_MICS2710_ID: - case SENSOR_MICS5525_ID: - wsPayloadModule(root, "mics"); - break; - default: - break; - } - #endif - + switch (sensor->getID()) { +#if HLW8012_SUPPORT + case SENSOR_HLW8012_ID: + wsPayloadModule(root, "hlw"); + break; +#endif +#if CSE7766_SUPPORT + case SENSOR_CSE7766_ID: + wsPayloadModule(root, "cse"); + break; +#endif +#if PZEM004T_SUPPORT || PZEM004TV30_SUPPORT + case SENSOR_PZEM004T_ID: + case SENSOR_PZEM004TV30_ID: + wsPayloadModule(root, "pzem"); + break; +#endif +#if PULSEMETER_SUPPORT + case SENSOR_PULSEMETER_ID: + wsPayloadModule(root, "pm"); + root["eneRatio0"] = ((PulseMeterSensor *) sensor)->getEnergyRatio(); + break; +#endif +#if MICS2710_SUPPORT || MICS5525_SUPPORT + case SENSOR_MICS2710_ID: + case SENSOR_MICS5525_ID: + wsPayloadModule(root, "mics"); + break; +#endif + } } - if (magnitudeCount()) { - root["snsRead"] = _sensor_read_interval / 1000; - root["snsReport"] = _sensor_report_every; - root["snsSave"] = _sensor_save_every; - _sensorWebSocketMagnitudesConfig(root); + root["snsRead"] = _sensor_read_interval / 1000; + root["snsReport"] = _sensor_report_every; + root["snsSave"] = _sensor_save_every; +} + +// Entries related to things reported by the module. +// - types of magnitudes that are available and the string values associated with them +// - error types and stringified versions of them +// - units are the value types of the magnitude +// TODO: magnitude types have some common keys and some specific ones, only implemented for the type +// e.g. voltMains is specific to the MAGNITUDE_VOLTAGE but *only* in analog mode, or eneRatio specific to MAGNITUDE_ENERGY +// but, notice that the sensor will probably be used to 'get' certain properties, to generate certain keys list +// TODO: report common keys either here or in the data payload +// some preprocessor magic might need to happen though, as prefixes are retrieved via `_magnitudeSettingsPrefix(type)` +// (also there is c++17 where string_view and char arrays may be concatenated at compile time) + +void _sensorWebSocketOnConnectedTypes(JsonObject& root) { + if (!_magnitudes.size()) { + return; } + + JsonObject& container = root.createNestedObject(F("magnitudesTypes")); + _sensorWebSocketTypes(container); + _sensorWebSocketErrors(container); + _sensorWebSocketUnits(container); +} + +// Entries specific to the sensor_magnitude_t; type, info, description + +void _sensorWebSocketOnConnectedConfig(JsonObject& root) { + if (!_magnitudes.size()) { + return; + } + + JsonObject& container = root.createNestedObject(F("magnitudesConfig")); + _sensorWebSocketConfig(container); } } // namespace // Used by modules to generate magnitude_id<->module_id mapping for the WebUI -// WS produces tuples Magnitudes that contain type, sensor's global index and module's index -// Settings use Magnitude keys to allow us to retrieve module's index +// Prefix controls the UI templates, supplied callback should retrieve module-specific value Id -void sensorWebSocketMagnitudes(JsonObject& root, const String& prefix) { - const String wsKey = prefix + F("Magnitudes"); - const String confKey = wsKey.substring(0, wsKey.length() - 1); +void sensorWebSocketMagnitudes(JsonObject& root, const char* prefix, SensorWebSocketMagnitudesCallback callback) { + ::web::ws::EnumerableConfig config{root, F("magnitudesModule")}; - JsonObject& namedList = root.createNestedObject(wsKey); + auto& container = config.root(); + container[F("prefix")] = prefix; - static const char* const keys[] PROGMEM = { - "type", "index_global", "index_module" - }; - - JsonArray& schema = namedList.createNestedArray("schema"); - schema.copyFrom(keys, sizeof(keys) / sizeof(*keys)); - - JsonArray& values = namedList.createNestedArray("values"); - for (size_t index = 0; index < _magnitudes.size(); ++index) { - JsonArray& tuple = values.createNestedArray(); - - auto& magnitude = _magnitudes[index]; - tuple.add(magnitude.type); - tuple.add(magnitude.index_global); - tuple.add(getSetting({confKey, index}, 0)); - } + config(F("values"), _magnitudes.size(), { + {F("type"), [](JsonArray& out, size_t index) { + out.add(_magnitudes[index].type); + }}, + {F("index_global"), [](JsonArray& out, size_t index) { + out.add(_magnitudes[index].index_global); + }}, + {F("index_module"), callback} + }); } #endif // WEB_SUPPORT @@ -2857,7 +3045,8 @@ void sensorSetup() { #if WEB_SUPPORT wsRegister() .onVisible(_sensorWebSocketOnVisible) - .onConnected(_sensorWebSocketOnConnected) + .onConnected(_sensorWebSocketOnConnectedTypes) + .onConnected(_sensorWebSocketOnConnectedConfig) .onData(_sensorWebSocketSendData) .onKeyCheck(_sensorWebSocketOnKeyCheck); #endif diff --git a/code/espurna/sensor.h b/code/espurna/sensor.h index bdf6df25..35268b19 100644 --- a/code/espurna/sensor.h +++ b/code/espurna/sensor.h @@ -154,7 +154,8 @@ String magnitudeName(unsigned char type); String sensorError(unsigned char error); -void sensorWebSocketMagnitudes(JsonObject& root, const String& prefix); +using SensorWebSocketMagnitudesCallback = void(*)(JsonArray&, size_t); +void sensorWebSocketMagnitudes(JsonObject& root, const char* prefix, SensorWebSocketMagnitudesCallback); unsigned char sensorCount(); void sensorSetup(); diff --git a/code/espurna/thingspeak.cpp b/code/espurna/thingspeak.cpp index 327db2cc..9a92beaf 100644 --- a/code/espurna/thingspeak.cpp +++ b/code/espurna/thingspeak.cpp @@ -109,7 +109,9 @@ void _tspkWebSocketOnConnected(JsonObject& root) { } #if SENSOR_SUPPORT - sensorWebSocketMagnitudes(root, "tspk"); + sensorWebSocketMagnitudes(root, "tspk", [](JsonArray& out, size_t index) { + out.add(getSetting({"tspkMagnitude", index}, "0")); + }); #endif } diff --git a/code/espurna/ws.cpp b/code/espurna/ws.cpp index c37357ec..cddbd71f 100644 --- a/code/espurna/ws.cpp +++ b/code/espurna/ws.cpp @@ -53,17 +53,25 @@ EnumerableConfig::EnumerableConfig(JsonObject& root, const __FlashStringHelper* _root(root.createNestedObject(name)) {} -void EnumerableConfig::operator()(const __FlashStringHelper* name, size_t count, Pairs&& pairs) +void EnumerableConfig::operator()(const __FlashStringHelper* name, Iota iota, Check check, Pairs&& pairs) { + if (!iota) { + return; + } + if (!_root.containsKey(FPSTR(SchemaKey))) { JsonArray& schema = _root.createNestedArray(FPSTR(SchemaKey)); internal::populateSchema(schema, pairs); JsonArray& entries = _root.createNestedArray(name); - for (size_t index = 0; index < count; ++index) { - JsonArray& entry = entries.createNestedArray(); - internal::populateEntry(entry, pairs, index); - } + do { + if (!check || check(*iota)) { + JsonArray& entry = entries.createNestedArray(); + internal::populateEntry(entry, pairs, (*iota)); + } + + ++iota; + } while (iota); } } diff --git a/code/espurna/ws_utils.h b/code/espurna/ws_utils.h index 9fbfcc44..ff4dda02 100644 --- a/code/espurna/ws_utils.h +++ b/code/espurna/ws_utils.h @@ -12,6 +12,31 @@ Copyright (C) 2019-2021 by Maxim Prokhorov 1, two0 => 2, three0 => 3, four0 => 4 +// one1 => 5, two1 => 6, three1 => 7, four1 => 8 +// ...etc... +// ``` +// Where each row in values is the specific index, and the key string is taken from the schema list +// Obviously, number of elements is always expected to match #include @@ -19,11 +44,63 @@ namespace web { namespace ws { // TODO: use `const char*', but somehow force the arduinojson layer to *always* use flash funcs for reading them? +// TODO: try to minimize the ROM by implementing things in .cpp +// TODO: generic templated funcs instead of pointers? also, ROM... struct EnumerableConfig { + struct Iota { + Iota() = default; + constexpr explicit Iota(size_t end) : + _it(0), + _end(end) + {} + + constexpr Iota(size_t begin, size_t end) : + _it(begin), + _end(end) + {} + + constexpr Iota(size_t begin, size_t end, size_t step) : + _it(begin), + _end(end), + _step(step) + {} + + constexpr Iota& operator++() { + if (_it != _end) { + _it = ((_it + _step) > _end) + ? _end : (_it + _step); + } + + return *this; + } + + constexpr Iota operator++(int) { + Iota out(*this); + ++out; + return out; + } + + constexpr explicit operator bool() const { + return _it != _end; + } + + constexpr size_t operator*() const { + return _it; + } + + private: + size_t _it { 0 }; + size_t _end { 0 }; + size_t _step { 1 }; + }; + + static_assert(std::is_trivially_copyable::value, ""); + alignas(4) static const char SchemaKey[]; + using Check = bool(*)(size_t); using Callback = void(*)(JsonArray&, size_t); struct Pair { @@ -34,7 +111,19 @@ struct EnumerableConfig { using Pairs = std::initializer_list; EnumerableConfig(JsonObject& root, const __FlashStringHelper* name); - void operator()(const __FlashStringHelper* name, size_t count, Pairs&&); + void operator()(const __FlashStringHelper* name, Iota, Check, Pairs&&); + + void operator()(const __FlashStringHelper* name, Iota iota, Pairs&& pairs) { + (*this)(name, iota, nullptr, std::move(pairs)); + } + + void operator()(const __FlashStringHelper* name, size_t end, Pairs&& pairs) { + (*this)(name, Iota{end}, nullptr, std::move(pairs)); + } + + JsonObject& root() { + return _root; + } private: JsonObject& _root; diff --git a/code/html/custom.css b/code/html/custom.css index a5d0556f..99f794c6 100644 --- a/code/html/custom.css +++ b/code/html/custom.css @@ -64,7 +64,6 @@ h2 { color:inherit; } -legend.module, .module { display: none; } diff --git a/code/html/custom.js b/code/html/custom.js index fec93c94..353525ed 100644 --- a/code/html/custom.js +++ b/code/html/custom.js @@ -65,8 +65,10 @@ var Rfm69 = { var Magnitudes = []; var MagnitudeErrors = {}; var MagnitudeNames = {}; +var MagnitudeUnits = {}; var MagnitudeTypePrefixes = {}; var MagnitudePrefixTypes = {}; + //endRemoveIf(!sensor) // ----------------------------------------------------------------------------- @@ -1272,21 +1274,24 @@ function createRelayList(values, container, template_name) { //removeIf(!sensor) -function createMagnitudeList(data, container, template_name) { - let target = document.getElementById(container); +function createMagnitudeList(data) { + const targetId = `${data.prefix}Magnitudes`; + + let target = document.getElementById(targetId); if (target.childElementCount > 0) { return; } data.values.forEach((values) => { - let [type, index_global, index_module] = values; + const entry = fromSchema(values, data.schema); - let line = loadConfigTemplate(template_name); + let line = loadConfigTemplate("module-magnitude"); line.querySelector("label").textContent = - MagnitudeNames[type].concat(" #").concat(parseInt(index_global, 10)); + `${MagnitudeNames[entry.type]} #${entry.index_global}`; line.querySelector("div.hint").textContent = - Magnitudes[index_global].description; + Magnitudes[entry.index_global].description; let input = line.querySelector("input"); - input.value = index_module; + input.name = `${data.prefix}Magnitude`; + input.value = entry.index_module; input.dataset["original"] = input.value; mergeTemplate(target, line); @@ -1559,12 +1564,7 @@ function initRelayConfig(id, cfg) { //removeIf(!sensor) -function initMagnitudes(data) { - let container = document.getElementById("magnitudes"); - if (container.childElementCount > 0) { - return; - } - +function initMagnitudesTypes(data) { data.types.values.forEach((cfg) => { const info = fromSchema(cfg, data.types.schema); MagnitudeNames[info.type] = info.name; @@ -1577,6 +1577,42 @@ function initMagnitudes(data) { MagnitudeErrors[error.type] = error.name; }); + data.units.values.forEach((cfg) => { + const unit = fromSchema(cfg, data.units.schema); + + // XXX: schema, too? + let options = []; + unit.supported.forEach(([id, name]) => { + MagnitudeUnits[id] = name; + options.push({id, name}); + }); + + // no need for the select when there's no choice + if (options.length < 2) { + return; + } + + let line = loadTemplate("sns-units"); + line.querySelector("label").textContent = + `${MagnitudeNames[unit.type]} #${unit.index_global}`; + + let select = line.querySelector("select"); + select.setAttribute("name", + `${MagnitudeTypePrefixes[unit.type]}Units${unit.index_global}`); + + initSelect(select, options); + setOriginalsFromValuesForNode(line, [select]); + + mergeTemplate(document.getElementById("sns-units-config"), line); + }); +} + +function initMagnitudes(data) { + let container = document.getElementById("magnitudes"); + if (container.childElementCount > 0) { + return; + } + data.magnitudes.values.forEach((cfg, index) => { const magnitude = fromSchema(cfg, data.magnitudes.schema); @@ -1584,7 +1620,7 @@ function initMagnitudes(data) { .concat(" #").concat(parseInt(magnitude.index_global, 10)); Magnitudes.push({ name: prettyName, - units: magnitude.units, + units: MagnitudeUnits[magnitude.units], description: magnitude.description }); @@ -1604,9 +1640,11 @@ function updateMagnitudes(data) { const magnitude = fromSchema(cfg, data.schema); let input = document.querySelector(`input[name='magnitude'][data-id='${id}']`); - input.value = (0 === magnitude.error) - ? (magnitude.value + Magnitudes[id].units) - : MagnitudeErrors[magnitude.error]; + input.value = (0 !== magnitude.error) + ? MagnitudeErrors[magnitude.error] + : (("nan" === magnitude.value) + ? "" + : `${magnitude.value}${Magnitudes[id].units}`); if (magnitude.info.length) { let info = input.parentElement.parentElement.querySelector("div.sns-info"); @@ -2195,11 +2233,21 @@ function processData(data) { //removeIf(!sensor) + if ("magnitudesTypes" === key) { + initMagnitudesTypes(value); + return; + } + if ("magnitudesConfig" === key) { initMagnitudes(value); return; } + if ("magnitudesModule" === key) { + createMagnitudeList(value); + return; + } + if ("magnitudes" === key) { updateMagnitudes(value); return; @@ -2295,41 +2343,19 @@ function processData(data) { } // --------------------------------------------------------------------- - // Domoticz + // Special mapping for domoticz and thingspeak // --------------------------------------------------------------------- - // Domoticz - Relays if ("dczRelays" === key) { createRelayList(value, "dczRelays", "dcz-relay"); return; } - // Domoticz - Magnitudes - //removeIf(!sensor) - if ("dczMagnitudes" === key) { - createMagnitudeList(value, "dczMagnitudes", "dcz-magnitude"); - return; - } - //endRemoveIf(!sensor) - - // --------------------------------------------------------------------- - // Thingspeak - // --------------------------------------------------------------------- - - // Thingspeak - Relays if ("tspkRelays" === key) { createRelayList(value, "tspkRelays", "tspk-relay"); return; } - // Thingspeak - Magnitudes - //removeIf(!sensor) - if ("tspkMagnitudes" === key) { - createMagnitudeList(value, "tspkMagnitudes", "tspk-magnitude"); - return; - } - //endRemoveIf(!sensor) - // --------------------------------------------------------------------- // General // --------------------------------------------------------------------- diff --git a/code/html/index.html b/code/html/index.html index a10b79b2..7e5ee9c7 100644 --- a/code/html/index.html +++ b/code/html/index.html @@ -1774,30 +1774,10 @@ -
- - -
+ -
- - -
- -
- - -
+
+ Calibration
@@ -1808,8 +1788,8 @@
-
- Energy monitor +
+ Energy monitor
@@ -1863,6 +1843,13 @@
+ +
+ Units +
+
+
+ @@ -2175,6 +2162,24 @@ + + + + + + - - - - - - - -