Skip to content

Commit 92dd6d2

Browse files
RomRiderdcapslock
andauthored
feat: Support for sections using section_mode: true (#959)
You can use this card in [sections views](https://www.home-assistant.io/dashboards/sections/) (the default in home assistant since early 2024). To enabled compatibility with sections (meaning the card adjusts its size automatically and aligns with the other cards), you need to add `section_mode: true` to the configuration of your card. This will set the CSS card height to 100% and allow modification of its size using the default `grid_options` available with all cards used in sections. For users with heavily modified cards using `styles`, you might need to adjust your configuration once enabling `section_mode`. ⚠️ While `section_mode` is enabled: using `aspect_ratio` or setting the card's `height` or `width` using CSS will probably the layout and is considered incompatible. There might be other incompatible options, if you find any, please update this documentation by submitting a PR. <img width="468" height="384" alt="section_mode" src="https://github.com/user-attachments/assets/c8bc53d2-a40c-4e44-93e7-34ded22dd575" /> ```yaml views: - title: Grid type: sections sections: - type: grid cards: - type: custom:button-card entity: switch.skylight name: Button 1 section_mode: true grid_options: rows: 4 columns: 6 - type: custom:button-card entity: switch.skylight name: Button 2 section_mode: true grid_options: rows: 2 columns: 6 - type: custom:button-card entity: switch.skylight name: Button 3 section_mode: true grid_options: rows: 2 columns: 6 - type: custom:button-card entity: switch.skylight name: Button 4 section_mode: true grid_options: rows: 2 columns: 12 ``` Closes #854 --------- Co-authored-by: dcapslock <[email protected]>
1 parent d77e263 commit 92dd6d2

File tree

8 files changed

+664
-14
lines changed

8 files changed

+664
-14
lines changed

README.md

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Lovelace Button card for your entities.
3232
- [Lock Object](#lock-object)
3333
- [State](#state)
3434
- [Available operators](#available-operators)
35+
- [Sections views](#sections-views)
3536
- [Layout](#layout)
3637
- [`triggers_update`](#triggers_update)
3738
- [Javascript Templates](#javascript-templates)
@@ -46,8 +47,8 @@ Lovelace Button card for your entities.
4647
- [Merging state by id](#merging-state-by-id)
4748
- [Variables](#variables)
4849
- [Installation](#installation)
50+
- [Installation and tracking with `HACS`](#installation-and-tracking-with-hacs)
4951
- [Manual Installation](#manual-installation)
50-
- [Installation and tracking with `hacs`](#installation-and-tracking-with-hacs)
5152
- [Examples](#examples)
5253
- [Configuration with states](#configuration-with-states)
5354
- [Default behavior](#default-behavior)
@@ -58,6 +59,7 @@ Lovelace Button card for your entities.
5859
- [Templates Support](#templates-support)
5960
- [Playing with label templates](#playing-with-label-templates)
6061
- [State Templates](#state-templates)
62+
- [Nested `custom:button-card`](#nested-custombutton-card)
6163
- [Styling](#styling)
6264
- [Lock](#lock)
6365
- [Aspect Ratio](#aspect-ratio)
@@ -99,6 +101,7 @@ Lovelace Button card for your entities.
99101
| `type` | string | **Required** | `custom:button-card` | Type of the card |
100102
| `template` | string | optional | any valid template from `button_card_templates` | See [configuration template](#Configuration-Templates) |
101103
| `entity` | string | optional | `switch.ac` | entity_id |
104+
| `section_mode` | boolean | optional | `true` \| `false` | Set it to `true` when the card is used in a sections view. See [Sections views](#sections-views) |
102105
| `triggers_update` | string or array | optional | `switch.ac` | entity_id list that would trigger a card update, see [triggers_update](#triggers_update) |
103106
| `group_expand` | boolean | false | `true` \| `false` | When `true`, if any of the entities triggering a card update is a group, it will auto-expand the group and the card will update on any child entity state change. This works with nested groups too. See [triggers_update](#triggers_update) |
104107
| `icon` | string | optional | `mdi:air-conditioner` | Icon to display. Will be overridden by the icon defined in a state (if present). Defaults to the entity icon. Hide with `show_icon: false`. Supports templates, see [templates](#javascript-templates) |
@@ -121,7 +124,7 @@ Lovelace Button card for your entities.
121124
| `show_last_changed` | boolean | `false` | `true` \| `false` | Replace the label altogether and display the the `last_changed` attribute in a nice way (eg: `12 minutes ago`) |
122125
| `show_entity_picture` | boolean | `false` | `true` \| `false` | Replace the icon by the entity picture (if any) or the custom picture (if any). Falls back to using the icon if both are undefined |
123126
| `show_live_stream` | boolean | `false` | `true` \| `false` | Display the camera stream (if the entity is a camera). Requires the `stream:` component to be enabled in home-assistant's config |
124-
| `live_stream_aspect_ratio` | string | optional | `16x9`, `50%`, `1.78` ... | See home-assistant Picture Entity card [aspect_ratio](https://www.home-assistant.io/dashboards/picture-entity/#aspect_ratio) for valid options |
127+
| `live_stream_aspect_ratio` | string | optional | `16x9`, `50%`, `1.78` ... | See home-assistant Picture Entity card [aspect_ratio](https://www.home-assistant.io/dashboards/picture-entity/#aspect_ratio) for valid options |
125128
| `live_stream_fit_mode` | string | optional | `cover`, `contain`, `fill` | See home-assistant Picture Entity card [fit_mod](https://www.home-assistant.io/dashboards/picture-entity/#fit_mode) for information on how each option works |
126129
| `entity_picture` | string | optional | Can be any of `/local/*` file or a URL | Will override the icon/the default entity_picture with your own image. Best is to use a square image. You can also define one per state. Supports templates, see [templates](#javascript-templates) |
127130
| `units` | string | optional | `Kb/s`, `lux`, ... | Override or define the units to display after the state of the entity. If omitted, it's using the entity's units |
@@ -163,7 +166,7 @@ tap_action:
163166
action: call-service
164167
service: light.turn_off
165168
target:
166-
entity_id: "[[[ return entity.entity_id ]]]"
169+
entity_id: '[[[ return entity.entity_id ]]]'
167170
```
168171
169172
Example - using a template for action:
@@ -173,16 +176,16 @@ type: custom:button-card
173176
variables:
174177
my_action_object: |
175178
[[[
176-
if (entity.state == "on")
177-
return {
178-
action: "call-service",
179-
service: "light.turn_off",
179+
if (entity.state == "on")
180+
return {
181+
action: "call-service",
182+
service: "light.turn_off",
180183
target: { entity_id: entity.entity_id }
181184
}
182185
else return { action: "none" }
183186
]]]
184187
entity: light.bed_light
185-
tap_action: "[[[ return variables.my_action_object ]]]"
188+
tap_action: '[[[ return variables.my_action_object ]]]'
186189
```
187190
188191
### Confirmation
@@ -274,6 +277,55 @@ The order of your elements in the `state` object matters. The first one which is
274277
| `template` | | See [templates](#javascript-templates) for examples. `value` needs to be a javascript expression which returns a boolean. If the boolean is true, it will match this state |
275278
| `default` | N/A | If nothing matches, this is used |
276279

280+
### Sections views
281+
282+
You can use this card in [sections views](https://www.home-assistant.io/dashboards/sections/) (the default in home assistant since early 2024).
283+
284+
To enable compatibility with sections (meaning the card adjusts its size automatically and aligns with the other cards), you need to add `section_mode: true` to the configuration of your card. This will set the CSS card height to 100% and allow modification of its size using the default `grid_options` available with all cards used in sections.
285+
286+
For users with heavily modified cards using `styles`, you might need to adjust your configuration once enabling `section_mode`.
287+
288+
⚠️ While `section_mode` is enabled: using `aspect_ratio` or setting the card's `height` or `width` using CSS will probably the layout and is considered incompatible. There might be other incompatible options, if you find any, please update this documentation by submitting a PR.
289+
290+
![section_mode_true](examples/section_mode.png)
291+
292+
```yaml
293+
views:
294+
- title: Grid
295+
type: sections
296+
sections:
297+
- type: grid
298+
cards:
299+
- type: custom:button-card
300+
entity: switch.skylight
301+
name: Button 1
302+
section_mode: true
303+
grid_options:
304+
rows: 4
305+
columns: 6
306+
- type: custom:button-card
307+
entity: switch.skylight
308+
name: Button 2
309+
section_mode: true
310+
grid_options:
311+
rows: 2
312+
columns: 6
313+
- type: custom:button-card
314+
entity: switch.skylight
315+
name: Button 3
316+
section_mode: true
317+
grid_options:
318+
rows: 2
319+
columns: 6
320+
- type: custom:button-card
321+
entity: switch.skylight
322+
name: Button 4
323+
section_mode: true
324+
grid_options:
325+
rows: 2
326+
columns: 12
327+
```
328+
277329
### Layout
278330

279331
This option enables you to modify the layout of the card.
@@ -1088,7 +1140,7 @@ name: '[[[ return variables.value; ]]]'
10881140

10891141
1. Download [button-card.js](http://www.github.com/custom-cards/button-card/releases/latest/download/button-card.js)
10901142
2. Place the file in your `config/www` folder
1091-
3. Add `/local/button_card.js` as a [dashboard JavaScript module resource](https://developers.home-assistant.io/docs/frontend/custom-ui/registering-resources/).
1143+
3. Add `/local/button_card.js` as a [dashboard JavaScript module resource](https://developers.home-assistant.io/docs/frontend/custom-ui/registering-resources/).
10921144

10931145
> Note: Your browser may block the download link as the file is a javascript file. If the link seems to do nothing, copy the link address and use directly in your browser's address bar where you will most likely get prompt on whether to allow the download or not.
10941146

@@ -1501,7 +1553,7 @@ Example with `template`:
15011553

15021554
#### Nested `custom:button-card`
15031555

1504-
A simple nested example. This could be completed with a non-nested card, but the simplicity of this example is to show using templates in nested button cards.
1556+
A simple nested example. This could be completed with a non-nested card, but the simplicity of this example is to show using templates in nested button cards.
15051557

15061558
```yaml
15071559
type: custom:button-card
@@ -1515,6 +1567,8 @@ custom_fields:
15151567
// template has 4 opening and closing '[]'
15161568
return entity?.state === 'on' ? 'Light On' : 'Light Off';
15171569
]]]]
1570+
1571+
15181572
styles:
15191573
grid:
15201574
- grid-template-areas: '"nested"'

examples/section_mode.png

10.3 KB
Loading

src/button-card.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import {
4444
computeCssVariable,
4545
isMediaSourceContentId,
4646
resolveMediaSource,
47+
findEntities,
48+
lovelaceViewIsSection,
4749
} from './helpers';
4850
import { createThing } from './common/create-thing';
4951
import { styles } from './styles';
@@ -142,6 +144,25 @@ class ButtonCard extends LitElement {
142144
return !!this._hass && !!this._config && this.isConnected;
143145
}
144146

147+
static getStubConfig(hass: HomeAssistant, entities: string[], entitiesFallback: string[]): ExternalButtonCardConfig {
148+
const maxEntities = 1;
149+
const foundEntities = findEntities(hass, maxEntities, entities, entitiesFallback, ['light', 'switch']);
150+
if (lovelaceViewIsSection()) {
151+
return {
152+
entity: foundEntities[0] || '',
153+
section_mode: true,
154+
grid_options: {
155+
rows: 2,
156+
columns: 6,
157+
},
158+
};
159+
}
160+
return {
161+
entity: foundEntities[0] || '',
162+
section_mode: false,
163+
};
164+
}
165+
145166
public set hass(hass: HomeAssistant) {
146167
this._hass = hass;
147168
Object.keys(this._cards).forEach((element) => {
@@ -978,7 +999,11 @@ class ButtonCard extends LitElement {
978999
const classList: ClassInfo = {
9791000
'button-card-main': true,
9801001
disabled: !this._isClickable(this._stateObj, configState),
1002+
section: !!this._config?.section_mode,
9811003
};
1004+
if (!!this._config?.section_mode) {
1005+
this.classList.add('section');
1006+
}
9821007
if (this._config?.tooltip) {
9831008
this.classList.add('tooltip');
9841009
}
@@ -1337,6 +1362,18 @@ class ButtonCard extends LitElement {
13371362
return this._config?.card_size || 3;
13381363
}
13391364

1365+
public getGridOptions() {
1366+
if (!this._config?.section_mode) {
1367+
return undefined;
1368+
}
1369+
return {
1370+
rows: 2,
1371+
columns: 6,
1372+
min_rows: 1,
1373+
min_columns: 1,
1374+
};
1375+
}
1376+
13401377
private _evalActions(config: ButtonCardConfig, action: string): ButtonCardConfig {
13411378
const configDuplicate = copy(config);
13421379
/* eslint no-param-reassign: 0 */

src/helpers.ts

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { PropertyValues } from 'lit';
22
import tinycolor, { TinyColor } from '@ctrl/tinycolor';
33
import { HomeAssistant, ResolvedMediaSource } from './types/homeassistant';
4-
import { LovelaceConfig } from './types/lovelace';
4+
import {
5+
LovelaceConfig,
6+
LovelaceViewConfig,
7+
MASONRY_VIEW_LAYOUT,
8+
PANEL_VIEW_LAYOUT,
9+
SECTION_VIEW_LAYOUTS,
10+
SECTIONS_VIEW_LAYOUT,
11+
} from './types/lovelace';
512
import { StateConfig } from './types/types';
613
import { HassEntity, HassEntityAttributeBase, HassEntityBase } from 'home-assistant-js-websocket';
714
import { OFF, UNAVAILABLE, isUnavailableState } from './common/const';
@@ -182,7 +189,7 @@ export function getLovelaceCast(): any {
182189
root = root && (root.querySelector('hui-view') || root.querySelector('hui-panel-view'));
183190
if (root) {
184191
const ll = root.lovelace;
185-
ll.current_view = root.___curView;
192+
ll.current_view = root?._curView ?? 0;
186193
return ll;
187194
}
188195
return null;
@@ -201,12 +208,39 @@ export function getLovelace(): LovelaceConfig | null {
201208
root = root && root.querySelector('hui-root');
202209
if (root) {
203210
const ll = root.lovelace;
204-
ll.current_view = root.___curView;
211+
ll.current_view = root?._curView ?? 0;
205212
return ll;
206213
}
207214
return null;
208215
}
209216

217+
export function getLovelaceView(): LovelaceViewConfig | undefined {
218+
const ll = getLovelace() || getLovelaceCast();
219+
return ll?.current_view ? ll.config?.views[ll.current_view] : undefined;
220+
}
221+
222+
export const getLovelaceViewType = (config?: LovelaceViewConfig): string => {
223+
if (config?.type) {
224+
return config.type;
225+
}
226+
if (config?.panel) {
227+
return PANEL_VIEW_LAYOUT;
228+
}
229+
if (config?.sections) {
230+
return SECTIONS_VIEW_LAYOUT;
231+
}
232+
if (config?.cards) {
233+
return MASONRY_VIEW_LAYOUT;
234+
}
235+
return SECTIONS_VIEW_LAYOUT;
236+
};
237+
238+
export const lovelaceViewIsSection = () => {
239+
const llView = getLovelaceView();
240+
const llViewType = getLovelaceViewType(llView);
241+
return SECTION_VIEW_LAYOUTS.includes(llViewType);
242+
};
243+
210244
export function slugify(value: string, delimiter = '_'): string {
211245
const a = 'àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;';
212246
const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`;
@@ -369,3 +403,64 @@ export const resolveMediaSource = (hass: HomeAssistant, media_content_id: string
369403
type: 'media_source/resolve_media',
370404
media_content_id,
371405
});
406+
407+
const arrayFilter = (array: any[], conditions: ((value: any) => boolean)[], maxSize: number) => {
408+
if (!maxSize || maxSize > array.length) {
409+
maxSize = array.length;
410+
}
411+
412+
const filteredArray: any[] = [];
413+
414+
for (let i = 0; i < array.length && filteredArray.length < maxSize; i++) {
415+
let meetsConditions = true;
416+
417+
for (const condition of conditions) {
418+
if (!condition(array[i])) {
419+
meetsConditions = false;
420+
break;
421+
}
422+
}
423+
424+
if (meetsConditions) {
425+
filteredArray.push(array[i]);
426+
}
427+
}
428+
429+
return filteredArray;
430+
};
431+
432+
export const findEntities = (
433+
hass: HomeAssistant,
434+
maxEntities: number,
435+
entities: string[],
436+
entitiesFallback: string[],
437+
includeDomains?: string[],
438+
entityFilter?: (stateObj: HassEntity) => boolean,
439+
) => {
440+
const conditions: ((value: string) => boolean)[] = [];
441+
442+
if (includeDomains?.length) {
443+
conditions.push((eid) => includeDomains!.includes(computeDomain(eid)));
444+
}
445+
446+
if (entityFilter) {
447+
conditions.push((eid) => hass.states[eid] && entityFilter(hass.states[eid]));
448+
}
449+
450+
const entityIds = arrayFilter(entities, conditions, maxEntities);
451+
452+
if (entityIds.length < maxEntities && entitiesFallback.length) {
453+
const fallbackEntityIds = findEntities(
454+
hass,
455+
maxEntities - entityIds.length,
456+
entitiesFallback,
457+
[],
458+
includeDomains,
459+
entityFilter,
460+
);
461+
462+
entityIds.push(...fallbackEntityIds);
463+
}
464+
465+
return entityIds;
466+
};

0 commit comments

Comments
 (0)