From 0e0e9eccf1c9993f9d8b14ca79d60d7c4e9975cb Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Tue, 24 Jan 2023 17:26:18 +0100 Subject: [PATCH] Add blacklist/remove entities #891 --- CHANGELOG_LATEST.md | 3 +- interface/src/i18n/de/index.ts | 1 + interface/src/i18n/en/index.ts | 1 + interface/src/i18n/fr/index.ts | 1 + interface/src/i18n/nl/index.ts | 1 + interface/src/i18n/no/index.ts | 1 + interface/src/i18n/pl/index.ts | 1 + interface/src/i18n/sv/index.ts | 1 + interface/src/project/OptionIcon.tsx | 6 +- .../src/project/SettingsCustomization.tsx | 210 +++++++++--------- interface/src/project/types.ts | 3 +- src/emsdevice.cpp | 51 +++-- src/emsdevicevalue.cpp | 8 + src/emsdevicevalue.h | 1 + src/web/WebCustomizationService.cpp | 51 ++++- 15 files changed, 207 insertions(+), 133 deletions(-) diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index d87aee0b3..c08f1d33d 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -32,7 +32,8 @@ - Add Rego 3000, TR120RF thermostats [#917](https://github.com/emsesp/EMS-ESP32/issues/917) - Add config for ESP32-S3 - Add heatpump silent mode and other entities [#896](https://github.com/emsesp/EMS-ESP32/issues/896) -- Allow reboot to other partition (factory or asymmetric OTA) +- Allow reboot to other partition (factory or asymetric OTA) +- blacklist entities to remove from memory [#891](https://github.com/emsesp/EMS-ESP32/issues/891) ## Fixed diff --git a/interface/src/i18n/de/index.ts b/interface/src/i18n/de/index.ts index b67789a95..c1eb13bc5 100644 --- a/interface/src/i18n/de/index.ts +++ b/interface/src/i18n/de/index.ts @@ -157,6 +157,7 @@ const de: Translation = { CUSTOMIZATIONS_HELP_3: 'Schreibaktion deaktivieren', CUSTOMIZATIONS_HELP_4: 'von MQTT und API ausschließen', CUSTOMIZATIONS_HELP_5: 'Aus dem Kontrollzentrum ausblenden', + CUSTOMIZATIONS_HELP_6: 'Aus dem Speicher löschen', SELECT_DEVICE: 'Wählen Sie ein Gerät aus', SET_ALL: 'setzen Sie alle', OPTIONS: 'Optionen', diff --git a/interface/src/i18n/en/index.ts b/interface/src/i18n/en/index.ts index 421815f82..629fbdef3 100644 --- a/interface/src/i18n/en/index.ts +++ b/interface/src/i18n/en/index.ts @@ -157,6 +157,7 @@ const en: Translation = { CUSTOMIZATIONS_HELP_3: 'disable write action', CUSTOMIZATIONS_HELP_4: 'exclude from MQTT and API', CUSTOMIZATIONS_HELP_5: 'hide from Dashboard', + CUSTOMIZATIONS_HELP_6: 'remove from memory', SELECT_DEVICE: 'Select a device', SET_ALL: 'set all', OPTIONS: 'Options', diff --git a/interface/src/i18n/fr/index.ts b/interface/src/i18n/fr/index.ts index f20e20a69..bf1335247 100644 --- a/interface/src/i18n/fr/index.ts +++ b/interface/src/i18n/fr/index.ts @@ -157,6 +157,7 @@ const fr: Translation = { CUSTOMIZATIONS_HELP_3: 'désactiver l\'action d\'écriture', CUSTOMIZATIONS_HELP_4: 'exclure de MQTT et de l\'API', CUSTOMIZATIONS_HELP_5: 'cacher du Tableau de bord', + CUSTOMIZATIONS_HELP_6: 'remove from memory', SELECT_DEVICE: 'Sélectionnez un appareil', SET_ALL: 'tout régler', OPTIONS: 'Options', diff --git a/interface/src/i18n/nl/index.ts b/interface/src/i18n/nl/index.ts index 95e58f6f8..9eb69397a 100644 --- a/interface/src/i18n/nl/index.ts +++ b/interface/src/i18n/nl/index.ts @@ -157,6 +157,7 @@ const nl: Translation = { CUSTOMIZATIONS_HELP_3: 'Zet schrijfacties uit', CUSTOMIZATIONS_HELP_4: 'Uitsluiten van MQTT en API', CUSTOMIZATIONS_HELP_5: 'verberg van het Dashboard', + CUSTOMIZATIONS_HELP_6: 'remove from memory', SELECT_DEVICE: 'Selecteer een apparaat', SET_ALL: 'Alles aanzetten', OPTIONS: 'Opties', diff --git a/interface/src/i18n/no/index.ts b/interface/src/i18n/no/index.ts index f597002d2..65d843130 100644 --- a/interface/src/i18n/no/index.ts +++ b/interface/src/i18n/no/index.ts @@ -157,6 +157,7 @@ const no: Translation = { CUSTOMIZATIONS_HELP_3: 'inaktiviser skriving', CUSTOMIZATIONS_HELP_4: 'ekskludere fra MQTT og API', CUSTOMIZATIONS_HELP_5: 'gjemme fra Dashboard', + CUSTOMIZATIONS_HELP_6: 'remove from memory', SELECT_DEVICE: 'Velg en enhet', SET_ALL: 'sett alle', OPTIONS: 'Alternativ', diff --git a/interface/src/i18n/pl/index.ts b/interface/src/i18n/pl/index.ts index 0cacf1b94..90b348239 100644 --- a/interface/src/i18n/pl/index.ts +++ b/interface/src/i18n/pl/index.ts @@ -157,6 +157,7 @@ const pl: BaseTranslation = { CUSTOMIZATIONS_HELP_3: 'zablokuj akcje zapisu', CUSTOMIZATIONS_HELP_4: 'wyklucz z MQTT i API', CUSTOMIZATIONS_HELP_5: 'ukryj na pulpicie', + CUSTOMIZATIONS_HELP_6: 'remove from memory', SELECT_DEVICE: 'wybierz urządzenie', SET_ALL: 'Ustaw wszystko jako', OPTIONS: 'Opcje', diff --git a/interface/src/i18n/sv/index.ts b/interface/src/i18n/sv/index.ts index 09e083895..65a4a4a05 100644 --- a/interface/src/i18n/sv/index.ts +++ b/interface/src/i18n/sv/index.ts @@ -157,6 +157,7 @@ const sv: Translation = { CUSTOMIZATIONS_HELP_3: 'Inaktivera skrivningar', CUSTOMIZATIONS_HELP_4: 'Exkludera från MQTT & API', CUSTOMIZATIONS_HELP_5: 'Göm från Kontrollpanel', + CUSTOMIZATIONS_HELP_6: 'remove from memory', SELECT_DEVICE: 'Välj en enhet', SET_ALL: 'ställ in alla', OPTIONS: 'Alternativ', diff --git a/interface/src/project/OptionIcon.tsx b/interface/src/project/OptionIcon.tsx index 71be44969..220f331c4 100644 --- a/interface/src/project/OptionIcon.tsx +++ b/interface/src/project/OptionIcon.tsx @@ -13,9 +13,13 @@ import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined'; import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined'; -type OptionType = 'readonly' | 'web_exclude' | 'api_mqtt_exclude' | 'favorite'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; + +type OptionType = 'deleted' | 'readonly' | 'web_exclude' | 'api_mqtt_exclude' | 'favorite'; const OPTION_ICONS: { [type in OptionType]: [React.ComponentType, React.ComponentType] } = { + deleted: [DeleteForeverIcon, DeleteOutlineIcon], readonly: [EditOffOutlinedIcon, EditOutlinedIcon], web_exclude: [VisibilityOffOutlinedIcon, VisibilityOutlinedIcon], api_mqtt_exclude: [CommentsDisabledOutlinedIcon, InsertCommentOutlinedIcon], diff --git a/interface/src/project/SettingsCustomization.tsx b/interface/src/project/SettingsCustomization.tsx index 6a104278d..777e3c734 100644 --- a/interface/src/project/SettingsCustomization.tsx +++ b/interface/src/project/SettingsCustomization.tsx @@ -17,6 +17,9 @@ import { Link } from '@mui/material'; +import { MessageBox } from '../components'; +import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; + import { Table } from '@table-library/react-table-library/table'; import { useTheme } from '@table-library/react-table-library/theme'; import { Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table'; @@ -41,11 +44,14 @@ import { extractErrorMessage, updateValue } from '../utils'; import { DeviceShort, Devices, DeviceEntity, DeviceEntityMask } from './types'; import { useI18nContext } from '../i18n/i18n-react'; +import RestartMonitor from '../framework/system/RestartMonitor'; export const APIURL = window.location.origin + '/api/'; const SettingsCustomization: FC = () => { const { LL } = useI18nContext(); + const [restarting, setRestarting] = useState(false); + const [restartNeeded, setRestartNeeded] = useState(false); const { enqueueSnackbar } = useSnackbar(); @@ -66,7 +72,7 @@ const SettingsCustomization: FC = () => { const entities_theme = useTheme({ Table: ` - --data-table-library_grid-template-columns: 120px repeat(1, minmax(80px, 1fr)) 45px 45px 120px; + --data-table-library_grid-template-columns: 150px repeat(1, minmax(80px, 1fr)) 45px 45px 120px; `, BaseRow: ` font-size: 14px; @@ -210,6 +216,9 @@ const SettingsCustomization: FC = () => { if ((m & 8) === 8) { new_masks.push('8'); } + if ((m & 128) === 128) { + new_masks.push('128'); + } return new_masks; }; @@ -249,6 +258,15 @@ const SettingsCustomization: FC = () => { } }; + const restart = async () => { + try { + await EMSESP.restart(); + setRestarting(true); + } catch (error) { + enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' }); + } + }; + const saveCustomization = async () => { if (devices && deviceEntities && selectedDevice !== -1) { const masked_entities = deviceEntities @@ -277,6 +295,8 @@ const SettingsCustomization: FC = () => { }); if (response.status === 200) { enqueueSnackbar(LL.CUSTOMIZATIONS_SAVED(), { variant: 'success' }); + } else if (response.status === 201) { + setRestartNeeded(true); } else { enqueueSnackbar(LL.PROBLEM_UPDATING(), { variant: 'error' }); } @@ -300,7 +320,8 @@ const SettingsCustomization: FC = () => { ={LL.CUSTOMIZATIONS_HELP_2()}   ={LL.CUSTOMIZATIONS_HELP_3()}   ={LL.CUSTOMIZATIONS_HELP_4()}   - ={LL.CUSTOMIZATIONS_HELP_5()} + ={LL.CUSTOMIZATIONS_HELP_5()}   + ={LL.CUSTOMIZATIONS_HELP_6()} { + + + @@ -467,53 +491,61 @@ const SettingsCustomization: FC = () => { {tableList.map((de: DeviceEntity) => ( editEntity(de)}> - { - de.m = getMaskNumber(mask); - if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) { - de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE; - } - if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) { - de.m = de.m & ~DeviceEntityMask.DV_FAVORITE; - } - setMasks(['']); - }} - > - - - - - - - - { + de.m = getMaskNumber(mask); + if (de.n === '' && (de.m & DeviceEntityMask.DV_READONLY)) { + de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE; } - /> - - - - - + if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) { + de.m = de.m & ~DeviceEntityMask.DV_FAVORITE; + } + setMasks(['']); + }} + > + + + + = 3}> + + + + + + + + + + + + + )} - {formatName(de)} - {!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)} - {!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)} - {formatValue(de.v)} + {!deviceEntity && (formatName(de))} + {!deviceEntity && !(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)} + {!deviceEntity && !(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)} + {!deviceEntity && (formatValue(de.v))} ))} @@ -552,25 +584,34 @@ const SettingsCustomization: FC = () => { {renderDeviceList()} {renderDeviceData()} - - + {restartNeeded && ( + + + + )} + {!restartNeeded && ( + + + + + + - - - - - + )} {renderResetDialog()} ); @@ -582,48 +623,7 @@ const SettingsCustomization: FC = () => { setDeviceEntity(undefined)}> {LL.EDIT() + ' ' + LL.ENTITY() + ' "' + de.id + '"'} - { - de.m = getMaskNumber(mask); - if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) { - de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE; - } - if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) { - de.m = de.m & ~DeviceEntityMask.DV_FAVORITE; - } - setMasks(['']); - }} - > - - - - - - - - - - - - - - - + {LL.DEFAULT(1) + ' ' + LL.NAME(1)}: {deviceEntity.n} @@ -689,7 +689,7 @@ const SettingsCustomization: FC = () => { return ( - {renderContent()} + {restarting ? : renderContent()} {renderEditDialog()} ); diff --git a/interface/src/project/types.ts b/interface/src/project/types.ts index db1ae49a4..9480a2f4a 100644 --- a/interface/src/project/types.ts +++ b/interface/src/project/types.ts @@ -298,5 +298,6 @@ export enum DeviceEntityMask { DV_WEB_EXCLUDE = 1, DV_API_MQTT_EXCLUDE = 2, DV_READONLY = 4, - DV_FAVORITE = 8 + DV_FAVORITE = 8, + DV_DELETED = 128 } diff --git a/src/emsdevice.cpp b/src/emsdevice.cpp index f421713ef..320a08aac 100644 --- a/src/emsdevice.cpp +++ b/src/emsdevice.cpp @@ -40,7 +40,16 @@ bool EMSdevice::has_entities() const { return true; } } - return false; + bool found = false; + EMSESP::webCustomizationService.read([&](WebCustomization & settings) { + for (EntityCustomization entityCustomization : settings.entityCustomizations) { + if (entityCustomization.device_id == device_id() && entityCustomization.entity_ids.size()) { + found = true; + break; + } + } + }); + return found; } std::string EMSdevice::tag_to_string(uint8_t tag, const bool translate) { @@ -511,14 +520,9 @@ void EMSdevice::add_device_value(uint8_t tag, for (std::string entity_id : entityCustomization.entity_ids) { // if there is an appended custom name, strip it to get the true entity name // and extract the new custom name - std::string shortname; auto custom_name_pos = entity_id.find('|'); bool has_custom_name = (custom_name_pos != std::string::npos); - if (has_custom_name) { - shortname = entity_id.substr(2, custom_name_pos - 2); - } else { - shortname = entity_id.substr(2); - } + std::string shortname = has_custom_name ? entity_id.substr(2, custom_name_pos - 2) : entity_id.substr(2); // we found the device entity if (shortname == entity) { @@ -1014,6 +1018,22 @@ void EMSdevice::generate_values_web_customization(JsonArray & output) { } } } + EMSESP::webCustomizationService.read([&](WebCustomization & settings) { + for (EntityCustomization entityCustomization : settings.entityCustomizations) { + if ((entityCustomization.device_id == device_id())) { + for (std::string entity_id : entityCustomization.entity_ids) { + uint8_t mask = Helpers::hextoint(entity_id.substr(0, 2).c_str()); + if (mask & 0x80) { + JsonObject obj = output.createNestedObject(); + obj["id"] = DeviceValue::get_name(entity_id); + obj["m"] = mask; + obj["w"] = false; + } + } + break; + } + } + }); } void EMSdevice::set_climate_minmax(uint8_t tag, int16_t min, uint16_t max) { @@ -1037,14 +1057,9 @@ void EMSdevice::setCustomEntity(const std::string & entity_id) { std::string entity_name = dv.tag < DeviceValueTAG::TAG_HC1 ? dv.short_name : tag_to_mqtt(dv.tag) + "/" + dv.short_name; // extra shortname - std::string shortname; auto custom_name_pos = entity_id.find('|'); bool has_custom_name = (custom_name_pos != std::string::npos); - if (has_custom_name) { - shortname = entity_id.substr(2, custom_name_pos - 2); - } else { - shortname = entity_id.substr(2); - } + std::string shortname = has_custom_name ? entity_id.substr(2, custom_name_pos - 2) : entity_id.substr(2); if (entity_name == shortname) { // check the masks @@ -1088,8 +1103,14 @@ void EMSdevice::getCustomEntities(std::vector & entity_ids) { for (const auto & dv : devicevalues_) { std::string entity_name = dv.tag < DeviceValueTAG::TAG_HC1 ? dv.short_name : tag_to_mqtt(dv.tag) + "/" + dv.short_name; uint8_t mask = dv.state >> 4; - - if (mask || !dv.custom_fullname.empty()) { + bool is_set = false; + for (auto & eid : entity_ids) { + if (DeviceValue::get_name(eid) == entity_name) { + is_set = true; + break; + } + } + if (!is_set && (mask || !dv.custom_fullname.empty())) { if (dv.custom_fullname.empty()) { entity_ids.push_back(Helpers::hextoa(mask, false) + entity_name); } else { diff --git a/src/emsdevicevalue.cpp b/src/emsdevicevalue.cpp index c6ce56bcc..3c2291716 100644 --- a/src/emsdevicevalue.cpp +++ b/src/emsdevicevalue.cpp @@ -378,4 +378,12 @@ std::string DeviceValue::get_fullname() const { return customname; } +std::string DeviceValue::get_name(std::string & entity) { + auto pos = entity.find('|'); + if (pos != std::string::npos) { + return entity.substr(2, pos - 2); + } + return entity.substr(2); +} + } // namespace emsesp diff --git a/src/emsdevicevalue.h b/src/emsdevicevalue.h index 16a2f0788..5d768b7e7 100644 --- a/src/emsdevicevalue.h +++ b/src/emsdevicevalue.h @@ -186,6 +186,7 @@ class DeviceValue { bool get_custom_max(uint16_t & val); std::string get_custom_fullname() const; std::string get_fullname() const; + static std::string get_name(std::string & entity); // dv state flags void add_state(uint8_t s) { diff --git a/src/web/WebCustomizationService.cpp b/src/web/WebCustomizationService.cpp index 10fd5b263..39efe4ccf 100644 --- a/src/web/WebCustomizationService.cpp +++ b/src/web/WebCustomizationService.cpp @@ -235,6 +235,7 @@ void WebCustomizationService::device_entities(AsyncWebServerRequest * request, J // saves it in the customization service // and updates the entity list real-time void WebCustomizationService::custom_entities(AsyncWebServerRequest * request, JsonVariant & json) { + bool need_reboot = false; if (json.is()) { // find the device using the unique_id for (const auto & emsdevice : EMSESP::emsdevices) { @@ -245,10 +246,47 @@ void WebCustomizationService::custom_entities(AsyncWebServerRequest * request, J uint8_t device_id = emsdevice->device_id(); // and set the mask and custom names immediately for any listed entities - JsonArray entity_ids_json = json["entity_ids"]; + JsonArray entity_ids_json = json["entity_ids"]; + std::vector entity_ids; for (const JsonVariant id : entity_ids_json) { - emsdevice->setCustomEntity(id.as()); + std::string id_s = id.as(); + if (id_s[0] == '8') { + entity_ids.push_back(id_s); + need_reboot = true; + } else { + emsdevice->setCustomEntity(id_s); + } + // emsesp::EMSESP::logger().info(id.as()); } + // add deleted entities from file + EMSESP::webCustomizationService.read([&](WebCustomization & settings) { + for (EntityCustomization entityCustomization : settings.entityCustomizations) { + if (entityCustomization.device_id == device_id) { + for (std::string entity_id : entityCustomization.entity_ids) { + uint8_t mask = Helpers::hextoint(entity_id.substr(0, 2).c_str()); + std::string name = DeviceValue::get_name(entity_id); + if (mask & 0x80) { + bool is_set = false; + for (const JsonVariant id : entity_ids_json) { + std::string id_s = id.as(); + id_s = DeviceValue::get_name(id_s); + if (id_s == name) { + is_set = true; + need_reboot = true; + break; + } + } + if (!is_set) { + entity_ids.push_back(entity_id); + } + } + } + break; + } + } + }); + // get list of entities that have masks set or a custom fullname + emsdevice->getCustomEntities(entity_ids); // Save the list to the customization file EMSESP::webCustomizationService.update( @@ -263,18 +301,11 @@ void WebCustomizationService::custom_entities(AsyncWebServerRequest * request, J } } - if (!entity_ids_json.size()) { - return StateUpdateResult::UNCHANGED; // nothing to add - } - // create a new entry for this device if there are values EntityCustomization new_entry; new_entry.product_id = product_id; new_entry.device_id = device_id; - // get list of entities that have masks set or a custom fullname - std::vector entity_ids; - emsdevice->getCustomEntities(entity_ids); new_entry.entity_ids = entity_ids; // add the record and save @@ -289,7 +320,7 @@ void WebCustomizationService::custom_entities(AsyncWebServerRequest * request, J } } - AsyncWebServerResponse * response = request->beginResponse(200); // OK + AsyncWebServerResponse * response = request->beginResponse(need_reboot ? 201 : 200); // OK request->send(response); }