Skip to content

Commit e284372

Browse files
committed
fix constant re-rendering on hass updates
1 parent 0da61df commit e284372

File tree

4 files changed

+134
-22
lines changed

4 files changed

+134
-22
lines changed

src/card.ts

Lines changed: 99 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,61 @@ import { addMarginForChips, entitiesThatShouldBeChips } from '@/helpers';
44
import { Task } from '@lit/task';
55
import type { Config } from '@type/config';
66
import type { HomeAssistant } from '@type/homeassistant';
7-
import { CSSResult, html, LitElement } from 'lit';
7+
import { CSSResult, html, LitElement, type TemplateResult } from 'lit';
88
import { state } from 'lit/decorators.js';
99
import { version } from '../package.json';
1010
import { chipStyles, styles } from './styles';
1111
const equal = require('fast-deep-equal');
1212

13+
/**
14+
* Declaration for the loadCardHelpers function provided by Home Assistant
15+
* Used to create and configure card elements
16+
*/
1317
declare function loadCardHelpers(): Promise<any>;
1418

19+
/**
20+
* Main component class for the Toolbar Status Chips
21+
* Displays entity statuses as chips in the Home Assistant toolbar
22+
*/
1523
export default class ToolbarStatusChips extends LitElement {
24+
/**
25+
* Card configuration object
26+
* @state Marks this as a reactive property that will trigger updates
27+
*/
1628
@state()
1729
private _config!: Config;
1830

31+
/**
32+
* Collection of entities to be displayed as chips
33+
* @state Marks this as a reactive property that will trigger updates
34+
*/
1935
@state()
2036
private _entities!: ChipEntity[];
2137

38+
/**
39+
* Current URL slug extracted from the document URL
40+
* Used for automatic area matching when no area is explicitly configured
41+
* @state Marks this as a reactive property that will trigger updates
42+
*/
2243
@state()
2344
private _slug: string | undefined;
2445

25-
// not state
46+
/**
47+
* Reference to the current Home Assistant instance
48+
* Not marked as @state as it's handled differently
49+
*/
2650
private _hass!: HomeAssistant;
2751

28-
// for editor
52+
/**
53+
* Flag indicating whether the card is in edit mode
54+
* Used by the Home Assistant card editor
55+
*/
2956
public editMode: boolean = false;
3057

58+
/**
59+
* Creates an instance of ToolbarStatusChips
60+
* Extracts the current URL slug and logs version information
61+
*/
3162
constructor() {
3263
super();
3364
this._slug = document?.URL?.split('?')[0]
@@ -41,7 +72,12 @@ export default class ToolbarStatusChips extends LitElement {
4172
);
4273
}
4374

44-
override render() {
75+
/**
76+
* Renders the lit element card
77+
* Displays chips based on the current entities if available
78+
* @returns {TemplateResult} The rendered HTML template
79+
*/
80+
override render(): TemplateResult {
4581
const styles = chipStyles(this.isEditing);
4682
return this._entities.length
4783
? this._createChipsTask.render({
@@ -54,35 +90,68 @@ export default class ToolbarStatusChips extends LitElement {
5490
: html``;
5591
}
5692

57-
// styles to position the status chips at the top of on the toolbar
93+
/**
94+
* Defines the CSS styles for the component
95+
* Positions the status chips at the top of the toolbar
96+
* @returns {CSSResult} The component styles
97+
*/
5898
static override get styles(): CSSResult {
5999
return styles;
60100
}
61101

62-
// config property getters
102+
/**
103+
* Gets the additional label from configuration
104+
* Used to filter entities by an additional label
105+
* @returns {string | undefined} The configured additional label
106+
*/
63107
get additionalLabel() {
64108
return this._config.additional_label;
65109
}
66110

111+
/**
112+
* Gets the area ID to filter entities by
113+
* Falls back to the URL slug if no area is configured
114+
* @returns {string | undefined} The area ID to use for filtering
115+
*/
67116
get area() {
68117
return this._config.area || this._slug;
69118
}
70119

120+
/**
121+
* Determines if optional entities should be shown
122+
* True if explicitly configured or if viewing the status path
123+
* @returns {boolean} Whether optional entities should be shown
124+
*/
71125
get optional() {
72126
return (
73127
this._config.features?.includes('optional') ||
74128
this.area === this.statusPath
75129
);
76130
}
77131

132+
/**
133+
* Gets the solo label from configuration
134+
* When set, only entities with this label are shown, ignoring area filtering
135+
* @returns {string | undefined} The configured solo label
136+
*/
78137
get soloLabel() {
79138
return this._config.solo_label;
80139
}
81140

141+
/**
142+
* Gets the status path from configuration
143+
* Defaults to 'home' if not configured
144+
* @returns {string} The path identifier for the status/home view
145+
*/
82146
get statusPath() {
83147
return this._config.status_path || 'home';
84148
}
85149

150+
/**
151+
* Determines if the card is currently in editing mode
152+
* True if editMode is explicitly set or if parent has 'preview' class
153+
* @returns {boolean} Whether the card is being edited
154+
*/
86155
get isEditing(): boolean {
87156
return (
88157
this.editMode ||
@@ -95,16 +164,22 @@ export default class ToolbarStatusChips extends LitElement {
95164
* HASS setup
96165
*/
97166

98-
// The user supplied configuration. Throw an exception and Home Assistant
99-
// will render an error card.
167+
/**
168+
* Sets the card configuration
169+
* Only updates if the new config is different from the current one
170+
* @param {Config} config - The new card configuration
171+
*/
100172
setConfig(config: Config) {
101173
if (!equal(config, this._config)) {
102174
this._config = config;
103175
}
104176
}
105177

106-
// Whenever the state changes, a new `hass` object is set. Use this to
107-
// update your content.
178+
/**
179+
* Updates the card when Home Assistant state changes
180+
* Filters entities based on labels and area, then creates chips for matching entities
181+
* @param {HomeAssistant} hass - The new Home Assistant state
182+
*/
108183
set hass(hass: HomeAssistant) {
109184
// get entities with the status label
110185
let entities = Object.values(hass.entities).filter((entity) =>
@@ -132,7 +207,6 @@ export default class ToolbarStatusChips extends LitElement {
132207

133208
const chips = entitiesThatShouldBeChips(entities, hass, this.optional);
134209

135-
// todo - don't set raw objects to properties of this...
136210
// check if the entities have changed - update the card
137211
if (!equal(chips, this._entities)) {
138212
// no need to check states if entities have changed
@@ -142,11 +216,20 @@ export default class ToolbarStatusChips extends LitElement {
142216
}
143217
}
144218

145-
// card configuration
219+
/**
220+
* Returns the configuration element for the card editor
221+
* @returns {HTMLElement} The card editor element
222+
*/
146223
static getConfigElement() {
147224
return document.createElement('toolbar-status-chips-editor');
148225
}
149226

227+
/**
228+
* Generates a default configuration based on Home Assistant state
229+
* Finds the area with the most status-labeled entities
230+
* @param {HomeAssistant} hass - The Home Assistant state
231+
* @returns {Promise<Config>} The generated configuration
232+
*/
150233
public static async getStubConfig(hass: HomeAssistant): Promise<Config> {
151234
// Get all area IDs and their details
152235
const areas = Object.entries(hass.areas);
@@ -191,7 +274,10 @@ export default class ToolbarStatusChips extends LitElement {
191274
};
192275
}
193276

194-
// Task handles async work
277+
/**
278+
* Task that creates the chip cards asynchronously
279+
* Uses the Home Assistant card helpers to create a horizontal stack of chips
280+
*/
195281
_createChipsTask = new Task(this, {
196282
task: async () => {
197283
const helpers = await loadCardHelpers();

src/entity.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class ChipEntity {
3636
*/
3737
get isActive(): boolean {
3838
const validation = this.validateState();
39-
return validation !== StateValidation.Pass;
39+
return validation === StateValidation.Error;
4040
}
4141

4242
/**
@@ -78,11 +78,17 @@ export class ChipEntity {
7878

7979
/**
8080
* Checks if a value is numeric
81+
* don't use ES6 syntax or this will be a key and break fast-deep-equal
82+
* @param num Value to check
83+
* @returns True if the value is numeric, false otherwise
8184
*/
82-
private isNumeric = (num: any) =>
83-
(typeof num === 'number' ||
84-
(typeof num === 'string' && num.trim() !== '')) &&
85-
!isNaN(num as number);
85+
private isNumeric(num: any): boolean {
86+
return (
87+
(typeof num === 'number' ||
88+
(typeof num === 'string' && num.trim() !== '')) &&
89+
!isNaN(num as number)
90+
);
91+
}
8692

8793
/**
8894
* Validates a state value against defined thresholds if numeric, or against 'on' state if non-numeric

test/card.spec.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,11 @@ describe('card.ts', () => {
123123
});
124124

125125
it('should use slug as area when area not provided', () => {
126-
stub(window, 'URL').returns({ pathname: 'foo/slug' } as any);
126+
const windowStub = stub(window, 'URL');
127+
windowStub.returns({ pathname: 'foo/slug' } as any);
127128
element.setConfig({});
128129
expect(element.area).to.equal((element as any)._slug);
130+
windowStub.restore();
129131
});
130132
});
131133

@@ -252,7 +254,27 @@ describe('card.ts', () => {
252254
});
253255

254256
it('should return true when parent has preview class', async () => {
255-
// todo - figure this out
257+
// Create a mock parent element
258+
const mockParentElement = {
259+
classList: {
260+
contains: (className: string) => className === 'preview',
261+
},
262+
};
263+
264+
// Use Object.defineProperty to mock the parentElement property
265+
Object.defineProperty(element, 'parentElement', {
266+
get: () => mockParentElement,
267+
configurable: true, // Allow the property to be redefined later
268+
});
269+
270+
// Verify isEditing returns true
271+
expect(element.isEditing).to.be.true;
272+
273+
// Clean up - restore the original property descriptor
274+
Object.defineProperty(element, 'parentElement', {
275+
value: null,
276+
configurable: true,
277+
});
256278
});
257279

258280
it('should return false when neither condition is met', () => {

test/editor.spec.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,6 @@ describe('editor.ts', () => {
6969
card.setConfig(testConfig);
7070

7171
const el = await fixture(card.render() as TemplateResult);
72-
console.log(el.innerHTML);
73-
console.log(el.outerHTML);
7472
expect(el.outerHTML).to.equal(
7573
`<div>
7674
<h4>

0 commit comments

Comments
 (0)