Skip to content

Commit

Permalink
Merge pull request #995 from MrBearPresident/Working-on-Conditional-V…
Browse files Browse the repository at this point in the history
…isibility-For-Subbutons

Conditional visibility for sub-buttons
  • Loading branch information
Clooos authored Dec 6, 2024
2 parents 2a5eb68 + 9ab0506 commit a51e122
Show file tree
Hide file tree
Showing 3 changed files with 294 additions and 0 deletions.
33 changes: 33 additions & 0 deletions src/editor/bubble-card-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
<ha-expansion-panel outlined>
Expand Down Expand Up @@ -1835,6 +1836,23 @@ export function createBubbleCardEditor() {
${this.makeTapActionPanel("Hold action", subButton, 'none', 'sub_button', index)}
</div>
</ha-expansion-panel>
<ha-expansion-panel outlined>
<h4 slot="header">
<ha-icon icon="mdi:eye"></ha-icon>
Visibility
</h4>
<div class="content">
<p class="intro">
The sub-button will be shown when ALL conditions below are fulfilled. If no conditions are set, the sub-button will always be shown.
</p>
<ha-card-conditions-editor
.hass=${this.hass}
.conditions=${conditions}
@value-changed=${(ev) => this._conditionChanged(index ,ev,'sub_button')}
>
</ha-card-conditions-editor>
</div>
</ha-expansion-panel>
</div>
</ha-expansion-panel>
`;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions src/tools/global-changes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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
Expand Down
250 changes: 250 additions & 0 deletions src/tools/validate-condition.ts
Original file line number Diff line number Diff line change
@@ -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);

0 comments on commit a51e122

Please sign in to comment.