Skip to content

Commit

Permalink
web: dynamic types and fpstr to sv conversions
Browse files Browse the repository at this point in the history
de-hardcode sch and led enumeration types from .html
publish raw type & pretty-string from .cpp indexed settings

de-fpstr string view references, use the object directly

publish 'faulty' schedule specs indexes, focus & report elems
ref. #2626

should fix faulty enum<->number references in selects
ref. #2628 - schedule types were numeric, while websocket delivered strings
  • Loading branch information
mcspr committed Nov 29, 2024
1 parent 017f499 commit 4423467
Show file tree
Hide file tree
Showing 12 changed files with 184 additions and 44 deletions.
7 changes: 7 additions & 0 deletions code/espurna/compat.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,13 @@ constexpr auto cend(const T& value) -> decltype(std::end(value)) {
}
#endif

#if __cplusplus < 202102L
template <typename Enum, typename Type = typename std::underlying_type<Enum>::type>
constexpr Type to_underlying(Enum value) {
return static_cast<Type>(value);
}
#endif

} // namespace std

// Same as min and max, force same type arguments
Expand Down
8 changes: 8 additions & 0 deletions code/espurna/led.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -995,11 +995,19 @@ bool onKeyCheck(StringView key, const JsonVariant&) {

void onVisible(JsonObject& root) {
wsPayloadModule(root, settings::Prefix);

espurna::web::ws::EnumerableTypes types{root, STRING_VIEW("ledModes")};
types(espurna::led::settings::options::LedModeOptions);
}

void onConnected(JsonObject& root) {
if (count()) {
espurna::web::ws::EnumerableConfig config{root, STRING_VIEW("ledConfig")};
config.replacement(
settings::query::internal::mode,
[](JsonArray& out, size_t index) {
out.add(std::to_underlying(settings::mode(index)));
});
config(STRING_VIEW("leds"), ::espurna::led::count(), settings::query::IndexedSettings);
}
}
Expand Down
10 changes: 9 additions & 1 deletion code/espurna/scheduler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1524,10 +1524,18 @@ void onVisible(JsonObject& root) {
for (const auto& pair : settings::Settings) {
root[pair.key()] = pair.value();
}

espurna::web::ws::EnumerableTypes types{root, STRING_VIEW("schTypes")};
types(settings::internal::Types);
}

void onConnected(JsonObject& root){
espurna::web::ws::EnumerableConfig config{ root, STRING_VIEW("schConfig") };
espurna::web::ws::EnumerableConfig config{root, STRING_VIEW("schConfig")};
config.replacement(
settings::internal::type,
[](JsonArray& out, size_t index) {
out.add(std::to_underlying(settings::type(index)));
});
config(STRING_VIEW("schedules"), settings::count(), settings::IndexedSettings);

auto& schedules = config.root();
Expand Down
63 changes: 47 additions & 16 deletions code/espurna/ws.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ namespace {

namespace internal {

PROGMEM_STRING(SchemaKey, "schema");
STRING_VIEW_INLINE(SchemaKey, "schema");

} // namespace internal

Expand All @@ -55,26 +55,47 @@ constexpr bool authentication() {
} // namespace

EnumerableConfig::EnumerableConfig(JsonObject& root, StringView name) :
_root(root.createNestedObject(FPSTR(name.c_str())))
_root(root.createNestedObject(name))
{}

void EnumerableConfig::replacement(SourceFunc source, TargetFunc target) {
_replacements.push_back(
Replacement{
.source = source,
.target = target,
});
}

void EnumerableConfig::operator()(StringView name, espurna::settings::Iota iota, Check check, Setting* begin, Setting* end) {
JsonArray& entries = _root.createNestedArray(FPSTR(name.c_str()));
JsonArray& entries = _root.createNestedArray(name);

if (_root.containsKey(FPSTR(internal::SchemaKey))) {
if (_root.containsKey(internal::SchemaKey)) {
return;
}

JsonArray& schema = _root.createNestedArray(FPSTR(internal::SchemaKey));
JsonArray& schema = _root.createNestedArray(internal::SchemaKey);
for (auto it = begin; it != end; ++it) {
schema.add(FPSTR((*it).prefix().c_str()));
schema.add((*it).prefix());
}

while (iota) {
if (!check || check(*iota)) {
JsonArray& entry = entries.createNestedArray();
for (auto it = begin; it != end; ++it) {
entry.add((*it).value(*iota));
auto func = (*it).func();

auto replacement = std::find_if(
_replacements.begin(),
_replacements.end(),
[&](const Replacement& replacement) {
return func == replacement.source;
});

if (replacement != _replacements.end()) {
(*replacement).target(entry, *iota);
} else {
entry.add(func(*iota));
}
}
}

Expand All @@ -83,22 +104,22 @@ void EnumerableConfig::operator()(StringView name, espurna::settings::Iota iota,
}

EnumerablePayload::EnumerablePayload(JsonObject& root, StringView name) :
_root(root.createNestedObject(FPSTR(name.c_str())))
_root(root.createNestedObject(name))
{}

void EnumerablePayload::operator()(StringView name, settings::Iota iota, Check check, Pairs&& pairs) {
JsonArray& entries = _root.createNestedArray(FPSTR(name.c_str()));
JsonArray& entries = _root.createNestedArray(name);

if (_root.containsKey(FPSTR(internal::SchemaKey))) {
if (_root.containsKey(internal::SchemaKey)) {
return;
}

JsonArray& schema = _root.createNestedArray(FPSTR(internal::SchemaKey));
JsonArray& schema = _root.createNestedArray(internal::SchemaKey);

const auto begin = std::begin(pairs);
const auto end = std::end(pairs);
for (auto it = begin; it != end; ++it) {
schema.add(FPSTR((*it).name.c_str()));
schema.add((*it).name);
}

while (iota) {
Expand All @@ -113,6 +134,16 @@ void EnumerablePayload::operator()(StringView name, settings::Iota iota, Check c
}
}

EnumerableTypes::EnumerableTypes(JsonObject& root, StringView name) :
_root(root.createNestedArray(name))
{}

void EnumerableTypes::operator()(int value, StringView text) {
auto& entry = _root.createNestedArray();
entry.add(value);
entry.add(text);
}

} // namespace ws
} // namespace web
} // namespace espurna
Expand Down Expand Up @@ -577,12 +608,12 @@ void _wsParse(AsyncWebSocketClient* client, uint8_t* payload, size_t length) {
JsonObject& data = root["data"];
if (data.success()) {
if (strcmp(action, "restore") == 0) {
const auto* message = settingsRestoreJson(data)
? PSTR("Changes saved, you should be able to reboot now")
: PSTR("Cound not restore the configuration, see the debug log for more information");
const auto message = settingsRestoreJson(data)
? STRING_VIEW("Changes saved, you should be able to reboot now")
: STRING_VIEW("Cound not restore the configuration, see the debug log for more information");

wsPost(client_id, [message](JsonObject& root) {
root[F("message")] = FPSTR(message);
root[F("message")] = message;
});
return;
}
Expand Down
36 changes: 36 additions & 0 deletions code/espurna/ws_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ struct EnumerableConfig {
using Check = bool(*)(size_t);
using Setting = const settings::query::IndexedSetting;

using SourceFunc = Setting::ValueFunc;
using TargetFunc = void (*)(JsonArray&, size_t);

EnumerableConfig(JsonObject& root, StringView name);

void operator()(StringView name, settings::Iota iota, Check check, Setting* begin, Setting* end);
Expand All @@ -104,10 +107,43 @@ struct EnumerableConfig {
return _root;
}

void replacement(SourceFunc, TargetFunc);

private:
struct Replacement {
SourceFunc source;
TargetFunc target;
};


std::vector<Replacement> _replacements;
JsonObject& _root;
};

struct EnumerableTypes {
template <typename T>
using Enumeration = settings::options::Enumeration<T>;

EnumerableTypes(JsonObject& root, StringView name);

template <typename T>
void operator()(const Enumeration<T>* begin, const Enumeration<T>* end) {
for (auto it = begin; it != end; ++it) {
(*this)((*it).numeric(), (*it).string());
}
}

template <typename T>
void operator()(const T& other) {
(*this)(std::begin(other), std::end(other));
}

void operator()(int, StringView);

private:
JsonArray& _root;
};

} // namespace ws
} // namespace web
} // namespace espurna
4 changes: 4 additions & 0 deletions code/html/src/led.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
addSimpleEnumerables,
groupSettingsOnAddElem,
variableListeners,
addEnumerables,
} from './settings.mjs';

import { addFromTemplate, addFromTemplateWithSchema } from './template.mjs';
Expand Down Expand Up @@ -40,6 +41,9 @@ function listeners() {
"ledConfig": (_, value) => {
onConfig(value);
},
"ledModes": (_, value) => {
addEnumerables("ledMode", value);
},
};
};

Expand Down
24 changes: 22 additions & 2 deletions code/html/src/local.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
import { updateVariables } from './settings.mjs';

export function init() {
updateVariables({
schTypes: [
[1, "disabled"],
[2, "calendar"],
[3, "relative"],
],
ledModes: [
[1, "wifi"],
[2, "relay"],
[3, "relay-inverse"],
[4, "findme"],
[5, "findme-wifi"],
[6, "on"],
[7, "off"],
[8, "relays"],
[9, "relays-wifi"],
[0, "manual"],
],
});
updateVariables({
ntpServer: "192.168.1.1",
ntpTZ: 'UTC-2',
Expand Down Expand Up @@ -72,8 +91,8 @@ export function init() {
'ledRelay',
],
leds: [
[15, 0, 'relay-inverse', 0],
[16, 0, 'relay', 1],
[15, 0, 3, 0],
[16, 0, 2, 1],
],
},
rfm69Topic: 'foo',
Expand Down Expand Up @@ -181,6 +200,7 @@ export function init() {
[2, 0, 'relay 0 1', '05,10:00'],
[2, 0, 'relay 1 2', '05:00'],
[1, 0, 'relay 2 0\nrelay 0 0', '10:00'],
[3, 0, 'mqtt.send "foo" "bar"', '15m after cal#2'],
],
schema: [
'schType',
Expand Down
19 changes: 8 additions & 11 deletions code/html/src/panel-led.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,14 @@ <h2>Notification LED configuration</h2>
<details>

<summary>Available modes</summary>
<ol class="pure-u-1 pure-u-lg-3-4">
<li><strong>WiFi status</strong> will blink at 1Hz when attempting to connect. If successfully connected it will briefly blink every 5 seconds if in STA mode, or every second if the device is still in AP mode.</li>
<li><strong>Follow switch</strong> will force the LED to follow the status of a given switch (defaults to the 1st switch, use the selector to change it).</li>
<li><strong>Inverse switch</strong> is same as the above, but using the inverse status as status of the LED.</li>
<li><strong>Find me</strong> will turn the LED ON when all relays are OFF. This is meant to locate the device at night.</li>
<li><strong>Find me &amp; WiFi</strong> will also follow the WiFi status, and stay mostly ON when relays are OFF and mostly OFF when any of them are ON.</li>
<li><strong>Relay status</strong> will turn the LED ON whenever any switch is ON, and OFF otherwise. This is global status notification.</li>
<li><strong>Relay status &amp; WiFi</strong> will follow the WiFi status but will stay mostly OFF when relays are OFF, and mostly ON when any of them are ON.</li>
<li><strong>Manually managed</strong> will let you manage the LED status through the API. For example, MQTT will subscribe to the "&lt;base_topic&gt;/led/0/set", sending a payload of 0, 1 or 2 (to toggle it) will change the LED status accordingly.</li>
<li><strong>Always ON</strong> and <strong>Always OFF</strong> modes are self-explanatory.</li>
</ol>
<ul class="pure-u-1 pure-u-lg-3-4">
<li><code>wifi</code><p>blink once per second when attempting to connect. If successfully connected it will briefly power the led ON every 5 seconds if in STA mode, or every second if the device is in AP mode.</p></li>
<li><code>relay</code> and <code>relay-inverse</code> - follow the status of a given switch (defaults to the 1st switch, use the selector to change it).</li>
<li><code>findme</code> and <code>findme-wifi</code> - turns the LED ON when all relays are OFF. This is meant to locate the device at night.</li>
<li><code>relays</code> and <code>relays-wifi</code> - turn the LED ON whenever any switch is ON, and OFF otherwise.</li>
<li><code>on</code> and <code>off</code> - LED is either permanently ON or OFF.</li>
<li><code>manual</code> - LED can be turned ON or OFF only through the API. For example, MQTT will subscribe to the "&lt;base_topic&gt;/led/0/set". Publishing "on", "off" or "toggle" to that topic will change the LED status accordingly.</li>
</ul>

</details>

Expand Down
29 changes: 28 additions & 1 deletion code/html/src/schedule.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { showPanelByName } from './core.mjs';
import { addFromTemplate, addFromTemplateWithSchema } from './template.mjs';
import { groupSettingsOnAddElem, variableListeners } from './settings.mjs';
import { addEnumerables, groupSettingsOnAddElem, variableListeners } from './settings.mjs';

/** @param {function(HTMLElement): void} callback */
function withSchedules(callback) {
Expand All @@ -26,6 +27,26 @@ function onConfig(value) {
});
}

/**
* @param {[number, string, string]} value
*/
function onValidate(value) {
withSchedules((elem) => {
const [id, key, message] = value;
const elems = /** @type {NodeListOf<HTMLInputElement>} */
(elem.querySelectorAll(`input[name=${key}]`));

if (id < elems.length) {
showPanelByName("sch");

const target = elems[id];
target.focus();
target.setCustomValidity(message);
target.reportValidity();
}
});
}

/**
* @returns {import('./settings.mjs').KeyValueListeners}
*/
Expand All @@ -34,6 +55,12 @@ function listeners() {
"schConfig": (_, value) => {
onConfig(value);
},
"schValidate": (_, value) => {
onValidate(value);
},
"schTypes": (_, value) => {
addEnumerables("schType", value);
},
};
}

Expand Down
14 changes: 13 additions & 1 deletion code/html/src/settings.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,10 @@ export function setOriginalsFromValuesForNode(node) {
setOriginalsFromValues(findInputOrSelect(node));
}

/**
* @typedef {[number, string]} EnumerableTuple
*/

/**
* automatically generate <select> options for know entities
* @typedef {{id: number, name: string}} EnumerableEntry
Expand Down Expand Up @@ -1090,9 +1094,17 @@ export function getEnumerables(name) {

/**
* @param {string} name
* @param {EnumerableEntry[]} enumerables
* @param {EnumerableEntry[] | EnumerableTuple[]} enumerables
*/
export function addEnumerables(name, enumerables) {
enumerables = enumerables.map((x) => {
if (Array.isArray(x)) {
return {id: x[0], name: x[1]};
}

return x;
});

Enumerable[name] = enumerables;
notifyEnumerables(name, enumerables);
}
Expand Down
Loading

0 comments on commit 4423467

Please sign in to comment.