diff --git a/interface/src/project/SettingsCustomization.tsx b/interface/src/project/SettingsCustomization.tsx index c2895e291..f3d36fcbb 100644 --- a/interface/src/project/SettingsCustomization.tsx +++ b/interface/src/project/SettingsCustomization.tsx @@ -137,7 +137,7 @@ const SettingsCustomization: FC = () => { }, [LL]); const setInitialMask = (data: DeviceEntity[]) => { - setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn }))); + setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma }))); }; const fetchDeviceEntities = async (unique_id: number) => { @@ -249,8 +249,14 @@ const SettingsCustomization: FC = () => { const saveCustomization = async () => { if (devices && deviceEntities && selectedDevice !== -1) { const masked_entities = deviceEntities - .filter((de) => de.m !== de.o_m || de.cn !== de.o_cn) - .map((new_de) => new_de.m.toString(16).padStart(2, '0') + new_de.id + (new_de.cn ? '|' + new_de.cn : '')); + .filter((de) => de.m !== de.o_m || de.cn !== de.o_cn || de.ma !== de.o_ma || de.mi !== de.o_mi) + .map((new_de) => + new_de.m.toString(16).padStart(2, '0') + + new_de.id + + ((new_de.cn || new_de.mi || new_de.ma) ? '|' : '') + + (new_de.cn ? new_de.cn : '') + + (new_de.mi ? '>' + new_de.mi : '') + + (new_de.ma ? '<' + new_de.ma : '')); // check size in bytes to match buffer in CPP, which is 4096 const bytes = new TextEncoder().encode(JSON.stringify(masked_entities)).length; @@ -331,7 +337,7 @@ const SettingsCustomization: FC = () => { setDeviceEntities((prevState) => { const newState = prevState.map((obj) => { if (obj.id === deviceEntity.id) { - return { ...obj, cn: deviceEntity.cn }; + return { ...obj, cn: deviceEntity.cn, mi: deviceEntity.mi, ma: deviceEntity.ma }; } return obj; }); @@ -627,6 +633,28 @@ const SettingsCustomization: FC = () => { onChange={updateValue(setDeviceEntity)} /> + {typeof de.v === 'number' && de.w && ( + <> + + + + + + + + )} diff --git a/interface/src/project/types.ts b/interface/src/project/types.ts index 7cac14bd7..5241cb315 100644 --- a/interface/src/project/types.ts +++ b/interface/src/project/types.ts @@ -142,6 +142,10 @@ export interface DeviceEntity { o_m?: number; // original mask before edits o_cn?: string; // original cn before edits w: boolean; // writeable + mi?: string; // min value + ma?: string; // max value + o_mi?: string; + o_ma?: string; } export interface CustomEntities { diff --git a/src/emsdevice.cpp b/src/emsdevice.cpp index 3c4ab5170..6e3546e61 100644 --- a/src/emsdevice.cpp +++ b/src/emsdevice.cpp @@ -472,6 +472,7 @@ void EMSdevice::add_device_value(uint8_t tag, // add the device entity devicevalues_.emplace_back( device_type_, tag, value_p, type, options, options_single, numeric_operator, short_name, fullname, custom_fullname, uom, has_cmd, min, max, state); + devicevalues_.back().set_custom_minmax(); // add a new command if it has a function attached if (!has_cmd) { @@ -873,7 +874,7 @@ void EMSdevice::generate_values_web(JsonObject & output) { // as generate_values_web() but stripped down to only show all entities and their state // this is used only for WebCustomizationService::device_entities() void EMSdevice::generate_values_web_customization(JsonArray & output) { - for (const auto & dv : devicevalues_) { + for (auto & dv : devicevalues_) { // also show commands and entities that have an empty full name JsonObject obj = output.createNestedObject(); @@ -958,8 +959,9 @@ void EMSdevice::generate_values_web_customization(JsonArray & output) { } // add the custom name, is optional - if (!dv.custom_fullname.empty()) { - obj["cn"] = dv.custom_fullname; + std::string custom_fullname = dv.get_custom_fullname(); + if (!custom_fullname.empty()) { + obj["cn"] = custom_fullname; } } else { // it's a command @@ -968,6 +970,16 @@ void EMSdevice::generate_values_web_customization(JsonArray & output) { obj["m"] = dv.state >> 4; // send back the mask state. We're only interested in the high nibble obj["w"] = dv.has_cmd; // if writable + + // set the custom min and max values if there are any + int16_t dv_set_min; + uint16_t dv_set_max; + if (dv.get_custom_min(dv_set_min)) { + obj["mi"] = dv_set_min; + } + if (dv.get_custom_max(dv_set_max)) { + obj["ma"] = dv_set_max; + } } } @@ -1009,7 +1021,7 @@ void EMSdevice::setCustomEntity(const std::string & entity_id) { } else { dv.custom_fullname = ""; } - + dv.set_custom_minmax(); return; } } @@ -1405,6 +1417,24 @@ bool EMSdevice::generate_values(JsonObject & output, const uint8_t tag_filter, c // else if (dv.type == DeviceValueType::CMD && output_target != EMSdevice::OUTPUT_TARGET::MQTT) { // json[name] = ""; // } + + // check for value outside min/max range and adapt the limits to avoid HA complains + // Should this also check for api output? + if ((output_target == OUTPUT_TARGET::MQTT) && (dv.min != 0 || dv.max != 0)) { + if (json[name].is() || json[name].is()) { + int v = json[name]; + if (fahrenheit) { + v = (v - (32 * (fahrenheit - 1))) / 1.8; // reset to °C + } + if (v < dv.min) { + dv.min = v; + dv.remove_state(DeviceValueState::DV_HA_CONFIG_CREATED); + } else if (v > dv.max) { + dv.max = v; + dv.remove_state(DeviceValueState::DV_HA_CONFIG_CREATED); + } + } + } } } } diff --git a/src/emsdevicevalue.cpp b/src/emsdevicevalue.cpp index 649d4fff8..72087864c 100644 --- a/src/emsdevicevalue.cpp +++ b/src/emsdevicevalue.cpp @@ -326,13 +326,58 @@ bool DeviceValue::get_min_max(int16_t & dv_set_min, int16_t & dv_set_max) { return false; // nothing changed, not supported } -// returns the translated fullname or the custom fullname (if provided) -// always returns a std::string -std::string DeviceValue::get_fullname() const { - if (custom_fullname.empty()) { - return Helpers::translated_word(fullname); +// extract custom min from custom_fullname +bool DeviceValue::get_custom_min(int16_t & v) { + auto min_pos = custom_fullname.find('>'); + bool has_min = (min_pos != std::string::npos); + uint8_t fahrenheit = !EMSESP::system_.fahrenheit() ? 0 : (uom == DeviceValueUOM::DEGREES) ? 2 : (uom == DeviceValueUOM::DEGREES_R) ? 1 : 0; + if (has_min) { + v = Helpers::atoint(custom_fullname.substr(min_pos + 1).c_str()); + if (fahrenheit) { + v = (v - (32 * (fahrenheit - 1))) / 1.8; // reset to °C + } + } + return has_min; +} + +// extract custom max from custom_fullname +bool DeviceValue::get_custom_max(uint16_t & v) { + auto max_pos = custom_fullname.find('<'); + bool has_max = (max_pos != std::string::npos); + uint8_t fahrenheit = !EMSESP::system_.fahrenheit() ? 0 : (uom == DeviceValueUOM::DEGREES) ? 2 : (uom == DeviceValueUOM::DEGREES_R) ? 1 : 0; + if (has_max) { + v = Helpers::atoint(custom_fullname.substr(max_pos + 1).c_str()); + if (fahrenheit) { + v = (v - (32 * (fahrenheit - 1))) / 1.8; // reset to °C + } + } + return has_max; +} + +// sets min max to stored custom values (if set) +void DeviceValue::set_custom_minmax() { + get_custom_min(min); + get_custom_max(max); +} + +std::string DeviceValue::get_custom_fullname() const { + auto min_pos = custom_fullname.find('>'); + auto max_pos = custom_fullname.find('<'); + auto minmax_pos = min_pos < max_pos ? min_pos : max_pos; + if (minmax_pos != std::string::npos) { + return custom_fullname.substr(0, minmax_pos); } return custom_fullname; } +// returns the translated fullname or the custom fullname (if provided) +// always returns a std::string +std::string DeviceValue::get_fullname() const { + std::string customname = get_custom_fullname(); + if (customname.empty()) { + return Helpers::translated_word(fullname); + } + return customname; +} + } // namespace emsesp diff --git a/src/emsdevicevalue.h b/src/emsdevicevalue.h index 6420692fa..269a476d4 100644 --- a/src/emsdevicevalue.h +++ b/src/emsdevicevalue.h @@ -179,6 +179,10 @@ class DeviceValue { bool hasValue() const; bool get_min_max(int16_t & dv_set_min, int16_t & dv_set_max); + void set_custom_minmax(); + bool get_custom_min(int16_t & v); + bool get_custom_max(uint16_t & v); + std::string get_custom_fullname() const; std::string get_fullname() const; // dv state flags