From 71d79c4e6d7bf2bcd416586278729ee5b84c5f3c Mon Sep 17 00:00:00 2001 From: MrBearPresident Date: Wed, 4 Dec 2024 21:25:49 +0100 Subject: [PATCH 1/4] Add expansion in editor for the conditions to be configured. --- src/editor/bubble-card-editor.ts | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/editor/bubble-card-editor.ts b/src/editor/bubble-card-editor.ts index 44fb2109..4cf4eae4 100644 --- a/src/editor/bubble-card-editor.ts +++ b/src/editor/bubble-card-editor.ts @@ -1764,6 +1764,7 @@ export function createBubbleCardEditor() { let formattedName = this.hass.formatEntityAttributeName(state, attributeName); return { label: formattedName, value: attributeName }; }).filter(attribute => this._selectable_attributes.includes(attribute.value)); + const conditions = subButton.visibility ?? []; return html` @@ -1835,6 +1836,23 @@ export function createBubbleCardEditor() { ${this.makeTapActionPanel("Hold action", subButton, 'none', 'sub_button', index)} + +

+ + Visibility +

+
+

+ The entity will be shown when ALL conditions below are fulfilled. If no conditions are set, the entity will always be shown. +

+ this._conditionChanged(index ,ev,'sub_button')} + > + +
+
`; @@ -2080,6 +2098,7 @@ export function createBubbleCardEditor() { let arrayCopy = [...this._config[array]]; arrayCopy[index] = arrayCopy[index] || {}; arrayCopy[index] = { ...arrayCopy[index], ...value }; + arrayCopy[index] = { ...arrayCopy[index].visibility, ...value }; this._config[array] = arrayCopy; fireEvent(this, "config-changed", { config: this._config }); this.requestUpdate(); @@ -2126,6 +2145,20 @@ export function createBubbleCardEditor() { this.requestUpdate(); } + _conditionChanged(index, ev, array) { + ev.stopPropagation(); + this._config[array] = this._config[array] || []; + let arrayCopy = [...this._config[array]]; + arrayCopy[index] = arrayCopy[index] || {}; + const conditions = ev.detail.value; + arrayCopy[index] = { ...arrayCopy[index], + visibility: conditions }; + this._config[array] = arrayCopy; + fireEvent(this, "config-changed", { config: this._config }); + this.requestUpdate(); + } + + static get styles() { return css` div { From 8605b3b6e50e99a4a318f76239437fb583392654 Mon Sep 17 00:00:00 2001 From: MrBearPresident Date: Wed, 4 Dec 2024 21:26:20 +0100 Subject: [PATCH 2/4] Add possibility to validate and test conditions --- src/tools/validate-condition.ts | 250 ++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 src/tools/validate-condition.ts diff --git a/src/tools/validate-condition.ts b/src/tools/validate-condition.ts new file mode 100644 index 00000000..53349627 --- /dev/null +++ b/src/tools/validate-condition.ts @@ -0,0 +1,250 @@ + + +function getValueFromEntityId(hass,value){ + try{ + if (hass.states[value]) { + return hass.states[value]?.state; + }} + catch{} + return undefined; +} + +function checkStateCondition(condition,hass){ + const state = + condition.entity && hass.states[condition.entity] + ? hass.states[condition.entity].state + : "unavailable"; + let value = condition.state ?? condition.state_not; + + // Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now) + if (Array.isArray(value)) { + const entityValues = value + .map((v) => getValueFromEntityId(hass, v)) + .filter((v) => v !== undefined); + value = [...value, ...entityValues]; + } else if (typeof value === "string") { + const entityValue = getValueFromEntityId(hass, value); + value = [value]; + if (entityValue) { + value.push(entityValue); + } + } + + return condition.state != null + ? ensureArray(value).includes(state) + : !ensureArray(value).includes(state); +} + +export function ensureArray(value) { + if (value === undefined || Array.isArray(value)) { + return value; + } + return [value]; +} + +function checkStateNumericCondition(condition,hass) { + const state = (condition.entity ? hass.states[condition.entity] : undefined) + ?.state; + let above = condition.above; + let below = condition.below; + + // Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now) + if (typeof above === "string") { + above = getValueFromEntityId(hass, above) ?? above; + } + if (typeof below === "string") { + below = getValueFromEntityId(hass, below) ?? below; + } + + const numericState = Number(state); + const numericAbove = Number(above); + const numericBelow = Number(below); + + if (isNaN(numericState)) { + return false; + } + + return ( + (condition.above == null || + isNaN(numericAbove) || + numericAbove < numericState) && + (condition.below == null || + isNaN(numericBelow) || + numericBelow > numericState) + ); +} + +function checkScreenCondition(condition) { + return condition.media_query + ? matchMedia(condition.media_query).matches + : false; +} + +function checkUserCondition(condition, hass) { + return condition.users && hass.user?.id + ? condition.users.includes(hass.user.id) + : false; +} + +function checkAndCondition(condition, hass) { + if (!condition.conditions) return true; + return checkConditionsMet(condition.conditions, hass); +} + +function checkOrCondition(condition, hass) { + if (!condition.conditions) return true; + return condition.conditions.some((c) => checkConditionsMet([c], hass)); +} + +/** + * Return the result of applying conditions + * @param conditions conditions to apply + * @param hass Home Assistant object + * @returns true if conditions are respected + */ +export function checkConditionsMet(conditions,hass) { + return conditions.every((c) => { + if ("condition" in c) { + switch (c.condition) { + case "screen": + return checkScreenCondition(c); + case "user": + return checkUserCondition(c, hass); + case "numeric_state": + return checkStateNumericCondition(c, hass); + case "and": + return checkAndCondition(c, hass); + case "or": + return checkOrCondition(c, hass); + default: + return checkStateCondition(c, hass); + } + } + return checkStateCondition(c, hass); + }); +} + +export function extractConditionEntityIds(conditions) { + const entityIds = new Set([]); + for (const condition of conditions) { + if (condition.condition === "numeric_state") { + if ( + typeof condition.above === "string" && + isValidEntityId(condition.above) + ) { + entityIds.add(condition.above); + } + if ( + typeof condition.below === "string" && + isValidEntityId(condition.below) + ) { + entityIds.add(condition.below); + } + } else if (condition.condition === "state") { + [ + ...(ensureArray(condition.state) ?? []), + ...(ensureArray(condition.state_not) ?? []), + ].forEach((state) => { + if (!!state && isValidEntityId(state)) { + entityIds.add(state); + } + }); + } else if ("conditions" in condition && condition.conditions) { + return new Set([ + ...entityIds, + ...extractConditionEntityIds(condition.conditions), + ]); + } + } + return entityIds; +} + +function validateStateCondition(condition) { + return ( + condition.entity != null && + (condition.state != null || condition.state_not != null) + ); +} + +function validateScreenCondition(condition) { + return condition.media_query != null; +} + +function validateUserCondition(condition) { + return condition.users != null; +} + +function validateAndCondition(condition) { + return condition.conditions != null; +} + +function validateOrCondition(condition) { + return condition.conditions != null; +} + +function validateNumericStateCondition(condition) { + return ( + condition.entity != null && + (condition.above != null || condition.below != null) + ); +} +/** + * Validate the conditions config for the UI + * @param conditions conditions to apply + * @returns true if conditions are validated + */ +export function validateConditionalConfig(conditions){ + return conditions.every((c) => { + if ("condition" in c) { + switch (c.condition) { + case "screen": + return validateScreenCondition(c); + case "user": + return validateUserCondition(c); + case "numeric_state": + return validateNumericStateCondition(c); + case "and": + return validateAndCondition(c); + case "or": + return validateOrCondition(c); + default: + return validateStateCondition(c); + } + } + return validateStateCondition(c); + }); +} + +/** + * Build a condition for filters + * @param condition condition to apply + * @param entityId base the condition on that entity + * @returns a new condition with entity id + */ +export function addEntityToCondition(condition, entityId) { + if ("conditions" in condition && condition.conditions) { + return { + ...condition, + conditions: condition.conditions.map((c) => + addEntityToCondition(c, entityId) + ), + }; + } + + if ( + condition.condition === "state" || + condition.condition === "numeric_state" + ) { + return { + ...condition, + entity: entityId, + }; + } + return condition; +} + + +const validEntityId = /^(\w+)\.(\w+)$/; + +export const isValidEntityId = (entityId) => + validEntityId.test(entityId); \ No newline at end of file From c64f277947b9039a69e19cb3a1c0997e4a7a5883 Mon Sep 17 00:00:00 2001 From: MrBearPresident Date: Wed, 4 Dec 2024 21:26:47 +0100 Subject: [PATCH 3/4] Add conditional visibility functionality --- src/tools/global-changes.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/tools/global-changes.ts b/src/tools/global-changes.ts index 390aa2fe..95f841ce 100644 --- a/src/tools/global-changes.ts +++ b/src/tools/global-changes.ts @@ -15,6 +15,7 @@ import { getIconColor, isColorLight } from './utils.ts'; +import {checkConditionsMet, validateConditionalConfig, ensureArray} from './validate-condition.ts'; export function changeState(context) { const state = context._hass.states[context.config.entity]; @@ -371,6 +372,16 @@ export function changeSubButtonState(context, container = context.content, appen subButtonElement.dropdownArrow.classList.remove('no-icon-select-arrow'); } } + + //Check visibility Conditions + const visibilityConditions = subButton.visibility; + if (visibilityConditions != undefined){ + const visibilityConditions_array = ensureArray(visibilityConditions); + if(validateConditionalConfig(visibilityConditions_array)){ + const is_visible= checkConditionsMet(visibilityConditions_array,context._hass); + !is_visible ? subButtonElement.classList.add('hidden') : subButtonElement.classList.remove('hidden'); + } + } }); // Update context.previousValues with current subButtons From 9ab05068ae85ab4bf39de7bb96c808fab219df5e Mon Sep 17 00:00:00 2001 From: MrBearPresident Date: Thu, 5 Dec 2024 23:21:15 +0100 Subject: [PATCH 4/4] Change word from entity to sub-button in editor to be more clear --- src/editor/bubble-card-editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor/bubble-card-editor.ts b/src/editor/bubble-card-editor.ts index 4cf4eae4..08351ab9 100644 --- a/src/editor/bubble-card-editor.ts +++ b/src/editor/bubble-card-editor.ts @@ -1843,7 +1843,7 @@ export function createBubbleCardEditor() {

- The entity will be shown when ALL conditions below are fulfilled. If no conditions are set, the entity will always be shown. + The sub-button will be shown when ALL conditions below are fulfilled. If no conditions are set, the sub-button will always be shown.