purifier-card 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,33 @@
1
- import { LitElement, html, nothing } from 'lit';
2
- import { hasConfigOrEntityChanged, fireEvent } from 'custom-card-helpers';
1
+ import { CSSResultGroup, LitElement, PropertyValues, html, nothing } from 'lit';
2
+ import { customElement, property, state } from 'lit/decorators.js';
3
+ import {
4
+ hasConfigOrEntityChanged,
5
+ fireEvent,
6
+ HomeAssistant,
7
+ ServiceCallRequest,
8
+ } from 'custom-card-helpers';
3
9
  import registerTemplates from 'ha-template';
10
+ import get from 'lodash/get';
4
11
  import localize from './localize';
5
12
  import styles from './styles.css';
6
- import { version } from '../package.json';
7
13
  import workingImg from './images/purifier-working.gif';
8
14
  import standbyImg from './images/purifier-standby.png';
9
- import './purifier-card-editor';
15
+
16
+ import {
17
+ PurifierCardConfig,
18
+ PurifierEntity,
19
+ SliderValue,
20
+ Template,
21
+ } from './types';
22
+ import buildConfig from './config';
10
23
 
11
24
  registerTemplates();
12
25
 
26
+ // String in the right side will be replaced by Rollup
27
+ const PKG_VERSION = 'PKG_VERSION_VALUE';
28
+
13
29
  console.info(
14
- `%c PURIFIER-CARD %c ${version} `,
30
+ `%c PURIFIER-CARD %c ${PKG_VERSION} `,
15
31
  'color: white; background: blue; font-weight: 700;',
16
32
  'color: blue; background: white; font-weight: 700;'
17
33
  );
@@ -19,112 +35,55 @@ console.info(
19
35
  if (!customElements.get('ha-icon-button')) {
20
36
  customElements.define(
21
37
  'ha-icon-button',
22
- class extends customElements.get('paper-icon-button') {}
38
+ class extends (customElements.get('paper-icon-button') ?? HTMLElement) {}
23
39
  );
24
40
  }
25
41
 
26
42
  const SUPPORT_PRESET_MODE = 8;
27
- class PurifierCard extends LitElement {
28
- static get properties() {
29
- return {
30
- hass: Object,
31
- config: Object,
32
- requestInProgress: Boolean,
33
- };
34
- }
43
+ @customElement('purifier-card')
44
+ export class PurifierCard extends LitElement {
45
+ @property({ attribute: false }) public hass!: HomeAssistant;
35
46
 
36
- static get styles() {
47
+ @state() private config!: PurifierCardConfig;
48
+ @state() private requestInProgress = false;
49
+
50
+ public static get styles(): CSSResultGroup {
37
51
  return styles;
38
52
  }
39
53
 
40
- static async getConfigElement() {
54
+ public static async getConfigElement() {
55
+ import('./editor');
41
56
  return document.createElement('purifier-card-editor');
42
57
  }
43
58
 
44
- static getStubConfig(hass, entities) {
45
- const [purifierEntity] = entities.filter(
46
- (eid) => eid.substr(0, eid.indexOf('.')) === 'fan'
47
- );
59
+ public static getStubConfig(
60
+ _: unknown,
61
+ entities: string[]
62
+ ): Partial<PurifierCardConfig> {
63
+ const [purifierEntity] = entities.filter((eid) => eid.startsWith('fan'));
48
64
 
49
65
  return {
50
- entity: purifierEntity || '',
66
+ entity: purifierEntity ?? '',
51
67
  };
52
68
  }
53
69
 
54
- get platform() {
55
- if (this.config.platform === undefined) {
56
- return 'xiaomi_miio_airpurifier';
57
- }
58
-
59
- return this.config.platform;
60
- }
61
-
62
- get entity() {
63
- return this.hass.states[this.config.entity];
64
- }
65
-
66
- get showPresetMode() {
67
- if (this.config.show_preset_mode === undefined) {
68
- return true;
69
- }
70
-
71
- return this.config.show_preset_mode;
72
- }
73
-
74
- get showName() {
75
- if (this.config.show_name === undefined) {
76
- return true;
77
- }
78
-
79
- return this.config.show_name;
70
+ public setConfig(config: Partial<PurifierCardConfig>) {
71
+ this.config = buildConfig(config);
80
72
  }
81
73
 
82
- get showState() {
83
- if (this.config.show_state === undefined) {
84
- return true;
85
- }
86
-
87
- return this.config.show_state;
88
- }
89
-
90
- get showToolbar() {
91
- if (this.config.show_toolbar === undefined) {
92
- return true;
93
- }
94
-
95
- return this.config.show_toolbar;
96
- }
97
-
98
- get compactView() {
99
- if (this.config.compact_view === undefined) {
100
- return false;
101
- }
102
-
103
- return this.config.compact_view;
104
- }
105
-
106
- setConfig(config) {
107
- if (!config.entity) {
108
- throw new Error(localize('error.missing_entity'));
109
- }
110
-
111
- const actions = config.actions;
112
- if (actions && Array.isArray(actions)) {
113
- console.warn(localize('warning.actions_array'));
114
- }
115
-
116
- this.config = config;
74
+ get entity(): PurifierEntity {
75
+ return this.hass.states[this.config.entity] as PurifierEntity;
117
76
  }
118
77
 
119
- getCardSize() {
78
+ public getCardSize() {
120
79
  return 2;
121
80
  }
122
81
 
123
- shouldUpdate(changedProps) {
124
- return hasConfigOrEntityChanged(this, changedProps);
82
+ protected shouldUpdate(changedProps: PropertyValues) {
83
+ return hasConfigOrEntityChanged(this, changedProps, false);
125
84
  }
126
85
 
127
- updated(changedProps) {
86
+ protected updated(changedProps: PropertyValues) {
128
87
  if (
129
88
  changedProps.get('hass') &&
130
89
  changedProps.get('hass').states[this.config.entity] !==
@@ -134,7 +93,7 @@ class PurifierCard extends LitElement {
134
93
  }
135
94
  }
136
95
 
137
- handleMore(entityId = this.entity.entity_id) {
96
+ private handleMore(entityId: string = this.entity.entity_id) {
138
97
  fireEvent(
139
98
  this,
140
99
  'hass-more-info',
@@ -148,36 +107,41 @@ class PurifierCard extends LitElement {
148
107
  );
149
108
  }
150
109
 
151
- handlePresetMode(e) {
152
- const preset_mode = e.target.getAttribute('value');
153
- this.callService('fan.set_preset_mode', { preset_mode });
154
- }
155
-
156
- handlePercentage(e) {
157
- const percentage = e.detail.value;
158
- this.callService('fan.set_percentage', { percentage });
159
- }
160
-
161
- callService(service, options = {}, isRequest = true) {
110
+ private callService(
111
+ service: ServiceCallRequest['service'],
112
+ options: ServiceCallRequest['serviceData'] = {},
113
+ request = true
114
+ ) {
162
115
  const [domain, name] = service.split('.');
163
116
  this.hass.callService(domain, name, {
164
117
  entity_id: this.config.entity,
165
118
  ...options,
166
119
  });
167
120
 
168
- if (isRequest) {
121
+ if (request) {
169
122
  this.requestInProgress = true;
170
123
  this.requestUpdate();
171
124
  }
172
125
  }
173
126
 
174
- renderPresetMode() {
127
+ private handlePresetMode(event: PointerEvent) {
128
+ const preset_mode = (<HTMLDivElement>event.target).getAttribute('value');
129
+ this.callService('fan.set_preset_mode', { preset_mode });
130
+ }
131
+
132
+ private handlePercentage(event: CustomEvent<SliderValue>) {
133
+ const percentage = event.detail.value;
134
+ this.callService('fan.set_percentage', { percentage });
135
+ }
136
+
137
+ private renderPresetMode(): Template {
175
138
  const {
176
- attributes: { preset_mode, preset_modes, supported_features },
139
+ attributes: { preset_mode, preset_modes, supported_features = 0 },
177
140
  } = this.entity;
178
141
 
179
142
  if (
180
- !this.showPresetMode ||
143
+ !preset_mode ||
144
+ !this.config.show_preset_mode ||
181
145
  !preset_modes ||
182
146
  !(supported_features & SUPPORT_PRESET_MODE)
183
147
  ) {
@@ -188,7 +152,7 @@ class PurifierCard extends LitElement {
188
152
 
189
153
  return html`
190
154
  <div class="preset-mode">
191
- <ha-button-menu @click="${(e) => e.stopPropagation()}">
155
+ <ha-button-menu @click="${(e: PointerEvent) => e.stopPropagation()}">
192
156
  <mmp-icon-button slot="trigger">
193
157
  <ha-icon icon="mdi:fan"></ha-icon>
194
158
  <span>
@@ -202,9 +166,9 @@ class PurifierCard extends LitElement {
202
166
  <mwc-list-item
203
167
  ?activated=${selected === index}
204
168
  value=${item}
205
- @click=${(e) => this.handlePresetMode(e)}
169
+ @click=${(e: PointerEvent) => this.handlePresetMode(e)}
206
170
  >
207
- ${localize(`preset_mode.${item}`) || item}
171
+ ${localize(`preset_mode.${item.toLowerCase()}`) || item}
208
172
  </mwc-list-item>
209
173
  `
210
174
  )}
@@ -213,19 +177,28 @@ class PurifierCard extends LitElement {
213
177
  `;
214
178
  }
215
179
 
216
- renderAQI() {
180
+ private renderAQI(): Template {
217
181
  const { aqi = {} } = this.config;
218
- const { entity_id, attribute = 'aqi', unit = 'AQI' } = aqi;
182
+ const { entity_id, attribute, unit = 'AQI' } = aqi;
219
183
 
220
- const value = entity_id
221
- ? this.hass.states[entity_id].state
222
- : this.entity.attributes[attribute];
184
+ let value = '';
223
185
 
224
- let prefix = '';
186
+ if (entity_id && attribute) {
187
+ value = get(this.hass.states[entity_id].attributes, attribute);
188
+ } else if (attribute) {
189
+ value = get(this.entity.attributes, attribute);
190
+ } else if (entity_id) {
191
+ value = this.hass.states[entity_id].state;
192
+ } else {
193
+ return nothing;
194
+ }
225
195
 
226
- if (value < 10) {
196
+ let prefix: Template = nothing;
197
+ const numericValue = Number(value);
198
+
199
+ if (numericValue < 10) {
227
200
  prefix = html`<span class="number-off">00</span>`;
228
- } else if (value < 100) {
201
+ } else if (numericValue < 100) {
229
202
  prefix = html`<span class="number-off">0</span>`;
230
203
  }
231
204
 
@@ -237,7 +210,7 @@ class PurifierCard extends LitElement {
237
210
  `;
238
211
  }
239
212
 
240
- renderSlider() {
213
+ private renderSlider(): Template {
241
214
  const {
242
215
  state,
243
216
  attributes: { percentage, percentage_step },
@@ -252,14 +225,13 @@ class PurifierCard extends LitElement {
252
225
  value=${percentage}
253
226
  step=${percentage_step}
254
227
  ?disabled="${disabled}"
255
- @value-changed=${(e) => this.handlePercentage(e)}
228
+ @value-changed=${(e: CustomEvent<SliderValue>) =>
229
+ this.handlePercentage(e)}
256
230
  >
257
231
  </round-slider>
258
232
  <img src=${image} alt="purifier is ${state}" class="image" />
259
233
  <div class="slider-center">
260
- <div class="slider-content">
261
- ${this.renderAQI()}
262
- </div>
234
+ <div class="slider-content">${this.renderAQI()}</div>
263
235
  <div class="slider-value">
264
236
  ${percentage ? `${percentage}%` : nothing}
265
237
  </div>
@@ -268,27 +240,27 @@ class PurifierCard extends LitElement {
268
240
  `;
269
241
  }
270
242
 
271
- renderControls() {
272
- return this.compactView ? this.renderAQI() : this.renderSlider();
243
+ private renderControls(): Template {
244
+ return this.config.compact_view ? this.renderAQI() : this.renderSlider();
273
245
  }
274
246
 
275
- renderName() {
247
+ private renderName(): Template {
276
248
  const {
277
249
  attributes: { friendly_name },
278
250
  } = this.entity;
279
251
 
280
- if (!this.showName) {
252
+ if (!this.config.show_name) {
281
253
  return nothing;
282
254
  }
283
255
 
284
256
  return html` <div class="friendly-name">${friendly_name}</div> `;
285
257
  }
286
258
 
287
- renderState() {
259
+ private renderState(): Template {
288
260
  const { state } = this.entity;
289
261
  const localizedState = localize(`state.${state}`) || state;
290
262
 
291
- if (!this.showState) {
263
+ if (!this.config.show_state) {
292
264
  return nothing;
293
265
  }
294
266
 
@@ -305,20 +277,26 @@ class PurifierCard extends LitElement {
305
277
  `;
306
278
  }
307
279
 
308
- renderStats() {
309
- const { stats = [] } = this.config;
280
+ private renderStats(): Template {
281
+ const statsList = this.config.stats || [];
310
282
 
311
- const statsList = stats || [];
312
-
313
- return statsList.map(
283
+ const stats = statsList.map(
314
284
  ({ entity_id, attribute, value_template, unit, subtitle }) => {
315
- if (!entity_id && !attribute && !value_template) {
285
+ if (!entity_id && !attribute) {
316
286
  return nothing;
317
287
  }
318
288
 
319
- const state = entity_id
320
- ? this.hass.states[entity_id].state
321
- : this.entity.attributes[attribute];
289
+ let state = '';
290
+
291
+ if (entity_id && attribute) {
292
+ state = get(this.hass.states[entity_id].attributes, attribute);
293
+ } else if (attribute) {
294
+ state = get(this.entity.attributes, attribute);
295
+ } else if (entity_id) {
296
+ state = this.hass.states[entity_id].state;
297
+ } else {
298
+ return nothing;
299
+ }
322
300
 
323
301
  const value = html`
324
302
  <ha-template
@@ -338,13 +316,15 @@ class PurifierCard extends LitElement {
338
316
  `;
339
317
  }
340
318
  );
319
+
320
+ return stats.length ? html`<div class="stats">${stats}</div>` : nothing;
341
321
  }
342
322
 
343
- renderToolbar() {
323
+ private renderToolbar(): Template {
344
324
  const { shortcuts = [] } = this.config;
345
325
  const { state, attributes } = this.entity;
346
326
 
347
- if (!this.showToolbar) {
327
+ if (!this.config.show_toolbar) {
348
328
  return nothing;
349
329
  }
350
330
 
@@ -400,28 +380,30 @@ class PurifierCard extends LitElement {
400
380
  `;
401
381
  }
402
382
 
403
- render() {
383
+ private renderUnavailable(): Template {
384
+ return html`
385
+ <ha-card>
386
+ <div class="preview not-available">
387
+ <div class="metadata">
388
+ <div class="not-available">
389
+ ${localize('common.not_available')}
390
+ </div>
391
+ <div>
392
+ </div>
393
+ </ha-card>
394
+ `;
395
+ }
396
+
397
+ protected render() {
404
398
  if (!this.entity) {
405
- return html`
406
- <ha-card>
407
- <div class="preview not-available">
408
- <div class="metadata">
409
- <div class="not-available">
410
- ${localize('common.not_available')}
411
- </div>
412
- <div>
413
- </div>
414
- </ha-card>
415
- `;
399
+ return this.renderUnavailable();
416
400
  }
417
401
 
418
402
  return html`
419
403
  <ha-card>
420
404
  <div class="preview">
421
405
  <div class="header">
422
- <div class="tips">
423
- ${this.renderPresetMode()}
424
- </div>
406
+ <div class="tips">${this.renderPresetMode()}</div>
425
407
  <ha-icon-button
426
408
  class="more-info"
427
409
  icon="mdi:dots-vertical"
@@ -435,7 +417,7 @@ class PurifierCard extends LitElement {
435
417
 
436
418
  <div class="metadata">${this.renderName()} ${this.renderState()}</div>
437
419
 
438
- <div class="stats">${this.renderStats()}</div>
420
+ ${this.renderStats()}
439
421
  </div>
440
422
 
441
423
  ${this.renderToolbar()}
@@ -444,7 +426,11 @@ class PurifierCard extends LitElement {
444
426
  }
445
427
  }
446
428
 
447
- customElements.define('purifier-card', PurifierCard);
429
+ declare global {
430
+ interface Window {
431
+ customCards?: unknown[];
432
+ }
433
+ }
448
434
 
449
435
  window.customCards = window.customCards || [];
450
436
  window.customCards.push({
package/src/styles.css CHANGED
@@ -23,6 +23,7 @@ ha-card {
23
23
  flex: 1;
24
24
  position: relative;
25
25
  padding: 0px;
26
+ overflow: hidden;
26
27
  }
27
28
 
28
29
  .fill-gap {
@@ -189,7 +190,7 @@ ha-card {
189
190
 
190
191
  .stats-block {
191
192
  cursor: pointer;
192
- margin: var(--pc-spacing) 0px;
193
+ padding: var(--pc-spacing) 0px;
193
194
  text-align: center;
194
195
  border-right: 1px solid var(--pc-divider-color);
195
196
  flex-grow: 1;
@@ -10,10 +10,10 @@
10
10
  "off": "Изключен"
11
11
  },
12
12
  "preset_mode": {
13
- "Auto": "Автоматичен режим",
14
- "Silent": "Тих режим",
15
- "Favorite": "Любима",
16
- "Fan": "Вентилатор"
13
+ "auto": "Автоматичен режим",
14
+ "silent": "Тих режим",
15
+ "favorite": "Любима",
16
+ "fan": "Вентилатор"
17
17
  },
18
18
  "error": {
19
19
  "missing_entity": "Избирането на обект е задължително!"
@@ -10,10 +10,10 @@
10
10
  "off": "Apagat"
11
11
  },
12
12
  "preset_mode": {
13
- "Auto": "Auto",
14
- "Silent": "Silenciós",
15
- "Favorite": "Favorit",
16
- "Fan": "Ventilador"
13
+ "auto": "Auto",
14
+ "silent": "Silenciós",
15
+ "favorite": "Favorit",
16
+ "fan": "Ventilador"
17
17
  },
18
18
  "error": {
19
19
  "missing_entity": "Cal especificar una entitat."
@@ -10,17 +10,14 @@
10
10
  "off": "Vypnuto"
11
11
  },
12
12
  "preset_mode": {
13
- "Auto": "Automatický",
14
- "Silent": "Noční",
15
- "Favorite": "Manuální",
16
- "Fan": "Větrák"
13
+ "auto": "Automatický",
14
+ "silent": "Noční",
15
+ "favorite": "Manuální",
16
+ "fan": "Větrák"
17
17
  },
18
18
  "error": {
19
19
  "missing_entity": "Je vyžadováno specifikování entity!"
20
20
  },
21
- "warning": {
22
- "actions_array": "VAROVÁNÍ: 'actions' je přejmenováno na 'shortcuts'"
23
- },
24
21
  "editor": {
25
22
  "entity": "Entity (povinné)",
26
23
  "compact_view": "Kompaktní zobrazení",
@@ -10,10 +10,10 @@
10
10
  "off": "Aus"
11
11
  },
12
12
  "preset_mode": {
13
- "Auto": "Auto",
14
- "Silent": "Leise",
15
- "Favorite": "Favorit",
16
- "Fan": "Lüfter"
13
+ "auto": "Auto",
14
+ "silent": "Leise",
15
+ "favorite": "Favorit",
16
+ "fan": "Lüfter"
17
17
  },
18
18
  "error": {
19
19
  "missing_entity": "Es muss eine Entity definiert werden!"
@@ -10,17 +10,14 @@
10
10
  "off": "Off"
11
11
  },
12
12
  "preset_mode": {
13
- "Auto": "Auto",
14
- "Silent": "Silent",
15
- "Favorite": "Favorite",
16
- "Fan": "Fan"
13
+ "auto": "Auto",
14
+ "silent": "Silent",
15
+ "favorite": "Favorite",
16
+ "fan": "Fan"
17
17
  },
18
18
  "error": {
19
19
  "missing_entity": "Specifying entity is required!"
20
20
  },
21
- "warning": {
22
- "actions_array": "WARNING: 'actions' is renamed to 'shortcuts'"
23
- },
24
21
  "editor": {
25
22
  "entity": "Entity (Required)",
26
23
  "compact_view": "Compact View",
@@ -10,10 +10,10 @@
10
10
  "off": "Éteint"
11
11
  },
12
12
  "preset_mode": {
13
- "Auto": "Auto",
14
- "Silent": "Nuit",
15
- "Favorite": "Favori",
16
- "Fan": "Manuel"
13
+ "auto": "Auto",
14
+ "silent": "Nuit",
15
+ "favorite": "Favori",
16
+ "fan": "Manuel"
17
17
  },
18
18
  "error": {
19
19
  "missing_entity": "Il est obligatoire de spécifier une entité!"
@@ -10,10 +10,10 @@
10
10
  "off": "Spento"
11
11
  },
12
12
  "preset_mode": {
13
- "Auto": "Auto",
14
- "Silent": "Silenzioso",
15
- "Favorite": "Preferito",
16
- "Fan": "Ventola"
13
+ "auto": "Auto",
14
+ "silent": "Silenzioso",
15
+ "favorite": "Preferito",
16
+ "fan": "Ventola"
17
17
  },
18
18
  "error": {
19
19
  "missing_entity": "É necessario specificare l'entità"
@@ -10,10 +10,10 @@
10
10
  "off": "Av"
11
11
  },
12
12
  "preset_mode": {
13
- "Auto": "Auto",
14
- "Silent": "Stille",
15
- "Favorite": "Favoritt",
16
- "Fan": "Vifte"
13
+ "auto": "Auto",
14
+ "silent": "Stille",
15
+ "favorite": "Favoritt",
16
+ "fan": "Vifte"
17
17
  },
18
18
  "error": {
19
19
  "missing_entity": "Spesifiserende enhet kreves!"
@@ -10,17 +10,14 @@
10
10
  "off": "Uit"
11
11
  },
12
12
  "preset_mode": {
13
- "Auto": "Auto",
14
- "Silent": "Stil",
15
- "Favorite": "Favoriet",
16
- "Fan": "Ventilator"
13
+ "auto": "Auto",
14
+ "silent": "Stil",
15
+ "favorite": "Favoriet",
16
+ "fan": "Ventilator"
17
17
  },
18
18
  "error": {
19
19
  "missing_entity": "Instellen van entity is verplicht!"
20
20
  },
21
- "warning": {
22
- "actions_array": "WARNING: 'actions' is hernoemd naar 'shortcuts'"
23
- },
24
21
  "editor": {
25
22
  "entity": "Entity (Verplicht)",
26
23
  "compact_view": "Compact View",
@@ -10,11 +10,11 @@
10
10
  "off": "Wyłączony"
11
11
  },
12
12
  "preset_mode": {
13
- "Auto": "Auto",
14
- "Silent": "Cichy",
15
- "Favorite": "Ulubiony",
16
- "Fan": "Wentylator",
17
- "Idle": "Bezczynny"
13
+ "auto": "Auto",
14
+ "silent": "Cichy",
15
+ "favorite": "Ulubiony",
16
+ "fan": "Wentylator",
17
+ "idle": "Bezczynny"
18
18
  },
19
19
  "error": {
20
20
  "missing_entity": "Wymagane jest zadeklarowanie encji!"