Compare commits

...

74 Commits

Author SHA1 Message Date
Bram Kragten
22c3132638 20240131.0 (#19595) 2024-01-31 19:02:00 +01:00
Bram Kragten
ce32de6e23 20240131.0 (#19594) 2024-01-31 19:00:50 +01:00
Bram Kragten
b6bc88e460 Merge branch 'dev' of https://github.com/home-assistant/frontend into dev 2024-01-31 18:55:23 +01:00
Bram Kragten
6e00806f1a Update state-control-circular-slider-style.ts 2024-01-31 18:54:56 +01:00
Bram Kragten
d9fa148c49 Merge branch 'master' into dev 2024-01-31 18:52:58 +01:00
Bram Kragten
939b3a8092 Update button card styles (#19591) 2024-01-31 18:48:58 +01:00
Bram Kragten
95920ba710 Default to error correction Q when there is a center image (#19593)
default to error correction Q when there is a center image
2024-01-31 18:48:45 +01:00
Bram Kragten
462ac79890 Bumped version to 20240131.0 2024-01-31 18:35:09 +01:00
Paul Bottein
601a165b2a Disable reorder for readonly automation and disabled block (#19592) 2024-01-31 17:48:22 +01:00
Bram Kragten
62bb9b1a87 Add QR code selector (#19588) 2024-01-31 16:31:54 +01:00
Paul Bottein
b60ba35a9f Don't allow dragging parent into child element in automation editor (#19589) 2024-01-31 16:28:20 +01:00
Bram Kragten
c97c3f2fc4 Fix disabled users picker (#19590)
fix disabled users picker
2024-01-31 15:26:37 +00:00
Bram Kragten
ed888200f9 Add support for re-auth flows in repairs (#19587) 2024-01-31 15:06:01 +01:00
Bram Kragten
f4859320eb Add icon to areas (#19585)
* Add icon to areas

* Fix gallery

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2024-01-31 13:18:43 +00:00
Marcel van der Veldt
b159f4c074 Add matter device info and actions (#19578)
* add matter device info panel (WIP)

* actually enable card on device page

* fix remove fabric

* add some translation labels

* add dialog to interview node

* do not show info for bridged devices

* first device action

* add ping node action and dialog

* ping should be always available

* update model for MatterCommissioningParameters

* add basic support for open commissioning window

* move fabric management to dialog

* review

* Add link to thread panel

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-01-31 14:16:21 +01:00
Cody C
b700e08d52 Simplify MFA onboarding styling & flow (#19584)
Align MFA onboarding flow

* Aligns QR Code to centre to make scanning on mobile easier
* Removes background from manual key text
* Re-adds spacing to bottom of form so it is not hard up against the code entry form
2024-01-31 10:51:07 +01:00
karwosts
c1bdd679ff Promote edit dashboard button out of overflow menu (#19345) 2024-01-31 10:45:10 +01:00
Paulus Schoutsen
b728b9efc4 Allow mobile apps to provide QR code functionality (#19570)
* Add QR code scanner to external bus

* Make `hasQRScanner` a version number
2024-01-31 10:44:21 +01:00
David F. Mulcahey
8acae63939 Update ZHA reconfigure device dialog to show accurate cluster configuration statuses (#19527) 2024-01-31 09:50:55 +01:00
Bram Kragten
374f5ee1be Update thread preferred router (#19580) 2024-01-31 00:58:03 +01:00
karwosts
528533a2dd Combine climate graph with temperature device_classes (#19485) 2024-01-31 00:33:28 +01:00
Maxim A
2b18db8525 Add Y axis limits options for historical charts (#19297)
* Add Y axis limit options for historical charts

* Fir formatting according to linter

* Revert statistic graph changes

* Cleanup local tests leftover

* Show fit Y fit option only if limits are set
2024-01-31 00:18:14 +01:00
Joni Käki-Mäkelä
6cd8ee9253 Disable pointer-events for tile-card .icon-container class that don't have a "button" role (#19497)
Set pointer-events to none for icon containers that don't have button role
2024-01-31 00:15:41 +01:00
karwosts
e45709fffc Fix map icon color (#19567)
* Fix map icon color

* different solution

* updates from code review
2024-01-30 23:57:35 +01:00
Simon Lamon
28c21b1041 Localize trigger state in automation editor (#19554)
* trigger state

* Lokalize

* don't change existing trigger

* space

* Fixes
2024-01-30 23:36:39 +01:00
G Johansson
0919f0e89e Add new TURN_ON and TURN_OFF Climate feature flags (#19523) 2024-01-30 15:11:08 +01:00
Bram Kragten
7ce9a937b1 20240104.0 (#19284) 2024-01-04 17:48:13 +01:00
Paul Bottein
456c011f3e Fix thermostat and humidifier card rendering when off (#19281)
* Fix thermostat and humidifier card rendering when off

* Fix action color
2024-01-04 17:44:58 +01:00
Bram Kragten
a31b9f1b4d Fix due date when no time in certain timezones (#19280)
* Fix due date when no time in certain timezones

* Update dialog-todo-item-editor.ts
2024-01-04 17:44:57 +01:00
Bram Kragten
a1cf18468b Remove overflow hidden from profile (#19279) 2024-01-04 17:44:56 +01:00
Bram Kragten
f147a5e909 fix valve entities row (#19278) 2024-01-04 17:44:55 +01:00
Franck Nijhof
8d541595b8 Update getStates to support valves (#19277) 2024-01-04 17:44:54 +01:00
Bram Kragten
32fd8270d7 Fix turning valve on/off (#19269) 2024-01-04 17:44:53 +01:00
Bram Kragten
efddbfcfa0 Fix circular progress size + fix bug in assist pipeline debug (#19268) 2024-01-04 17:44:53 +01:00
karwosts
0b20725f5f Fix select view dialog (#19267)
* Fix select view dialog

* add import
2024-01-04 17:44:52 +01:00
renovate[bot]
030566c1e8 Update dependency marked to v11.1.1 (#19254) 2024-01-04 17:44:51 +01:00
Bram Kragten
fef2c44cb8 Bumped version to 20240104.0 2024-01-04 17:44:16 +01:00
Bram Kragten
b99b13251f 20240103.3 (#19263) 2024-01-03 15:03:00 +01:00
Bram Kragten
288d173a4d Bumped version to 20240103.3 2024-01-03 15:02:14 +01:00
Bram Kragten
2c69fe8c53 Fix checking todo item that dont support due date (#19262)
* Fix checking todo item that dont support due date

* make cleaner

* Revert "make cleaner"

This reverts commit fa33b33614.

* Update dialog-todo-item-editor.ts

* do check in 1 place
2024-01-03 15:00:24 +01:00
Paul Bottein
62dafac72b Display edit button for climate fan mode feature (#19259) 2024-01-03 15:00:23 +01:00
karwosts
22929672a0 Remove tile pointer/ripple/index when it has no action (#19137)
* Remove tile pointer/ripple/index when it has no action

* update

* Apply suggestions from code review

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2024-01-03 15:00:22 +01:00
Bram Kragten
ae0eac3415 Bumped version to 20240103.0 2024-01-03 15:00:06 +01:00
Bram Kragten
4ea7d826bc 20240103.1 (#19258) 2024-01-03 12:37:44 +01:00
Bram Kragten
336214d97f Revert conditional rendering of condition (#19257)
* Fix conditionally showing `triggered by`

* revert conditional rendering

* Update add-automation-element-dialog.ts

* Update add-automation-element-dialog.ts
2024-01-03 12:29:30 +01:00
Bram Kragten
c9a0ae6e2d Bumped version to 20240103.1 2024-01-03 12:29:10 +01:00
Bram Kragten
0bc69fb9b3 20240103.0 (#19256) 2024-01-03 10:55:23 +01:00
Bram Kragten
6e7366bf69 Calculate used domains on open of action dialog (#19255) 2024-01-03 10:38:51 +01:00
Bram Kragten
8ee4aa9e63 Bumped version to 20240103.0 2024-01-03 10:38:30 +01:00
Bram Kragten
386c3ea1ca 20240102.0 (#19234) 2024-01-02 20:02:03 +01:00
Simon Lamon
c2f3e43ee5 Set default values for required and disabled for labeled slider (#19246)
Set default values
2024-01-02 20:01:15 +01:00
Bram Kragten
7354988ec9 Move notification services to main list (#19235) 2024-01-02 20:01:14 +01:00
Bram Kragten
53dedc6c65 Bumped version to 20240102.0 2024-01-02 18:49:50 +01:00
Bram Kragten
de3b9a5bb2 Give todo and calendar edit static header (#19233) 2024-01-02 18:48:59 +01:00
Bram Kragten
8d496e1511 Update add-automation-element-dialog.ts 2024-01-02 18:48:06 +01:00
JLo
01bd88ce10 New copy for device trigger in automation editor (#19232)
New copy for device trigger in automation editor: 
"Set of conditions provided by your device. Great way to start."
2024-01-02 18:44:33 +01:00
Josh McCarty
18b5fd59a6 Add missing device classes for entity-registry-settings-editor (#19231)
* Add connectivity device class for binary sensors

* Add update device class

* Separate connectivity and update
2024-01-02 18:44:32 +01:00
Bram Kragten
750c1d5013 Change format of service description (#19229) 2024-01-02 18:44:31 +01:00
Bram Kragten
f2226cdec2 Use brand icons in actions (#19227) 2024-01-02 18:44:30 +01:00
Bram Kragten
c125ec087a Remove references to "service call" from actions (#19226) 2024-01-02 18:44:29 +01:00
Bram Kragten
f099f66065 Automation editor tweaks (#19225)
* Automation editor tweaks

* fix styling
2024-01-02 18:44:28 +01:00
JLo
4fcf99faa7 Review on automation editor text (#19223)
- Added `.` to bloc descriptions
- Changed "Other" into "OTher triggers" "Other conditions" and "Other actions"
- Adapted a few descriptions
2024-01-02 18:44:28 +01:00
karwosts
2add88ccc2 Localize a device action string (#19203) 2024-01-02 18:44:27 +01:00
Bram Kragten
aa94ec7949 20240101.0 (#19217) 2024-01-01 14:08:07 +01:00
Bram Kragten
7b4ecfd30a 20231228.0 (#19170) 2023-12-28 15:34:42 +01:00
Bram Kragten
9d9e789f4b 20231227.0 (#19157) 2023-12-27 17:29:11 +01:00
Paul Bottein
6ce613acd2 20231208.2 (#18971) 2023-12-08 14:50:28 +01:00
Paul Bottein
aa38e2d409 20231208.1 (#18962) 2023-12-08 10:36:45 +01:00
Paul Bottein
fce4e5e382 20231206.0 (#18925) 2023-12-06 14:24:48 +01:00
Bram Kragten
eb5e7ba3f3 20231205.0 (#18916) 2023-12-05 18:10:37 +01:00
Bram Kragten
ae2e8e7402 20231204.0 (#18882) 2023-12-04 12:10:33 +01:00
Bram Kragten
b854d23431 20231130.0 (#18843) 2023-11-30 17:19:47 +01:00
Bram Kragten
ef735d65cf 20231129.1 (#18811) 2023-11-29 15:31:24 +01:00
Bram Kragten
2803e6aa95 20231129.0 (#18809) 2023-11-29 12:53:12 +01:00
61 changed files with 2058 additions and 609 deletions

View File

@@ -1,39 +0,0 @@
diff --git a/modular/sortable.complete.esm.js b/modular/sortable.complete.esm.js
index 02e9f2d6bebeb430fe6e7c1cc3f9c3c9df051f14..bb8268b0844a1faa4108cc92c0be2a3dbaf23f83 100644
--- a/modular/sortable.complete.esm.js
+++ b/modular/sortable.complete.esm.js
@@ -1657,7 +1657,7 @@ Sortable.prototype =
target = parent; // store last element
}
/* jshint boss:true */
- while (parent = parent.parentNode);
+ while (parent = parent.parentNode || parent.getRootNode().host);
}
_unhideGhostForTarget();
diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js
index b04c8b4634f7c6b4ef1aadbb48afe6564306dea9..39a107163c8c336ebd669b5ea8a936af87e1c1e7 100644
--- a/modular/sortable.core.esm.js
+++ b/modular/sortable.core.esm.js
@@ -1657,7 +1657,7 @@ Sortable.prototype =
target = parent; // store last element
}
/* jshint boss:true */
- while (parent = parent.parentNode);
+ while (parent = parent.parentNode || parent.getRootNode().host);
}
_unhideGhostForTarget();
diff --git a/modular/sortable.esm.js b/modular/sortable.esm.js
index 6ec7ed1bb557e21c2578200161e989c65d23150b..0a05475a22904472fac6c13f524c674da76584b0 100644
--- a/modular/sortable.esm.js
+++ b/modular/sortable.esm.js
@@ -1657,7 +1657,7 @@ Sortable.prototype =
target = parent; // store last element
}
/* jshint boss:true */
- while (parent = parent.parentNode);
+ while (parent = parent.parentNode || parent.getRootNode().host);
}
_unhideGhostForTarget();

View File

@@ -0,0 +1,73 @@
diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js
index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b441f523f 100644
--- a/modular/sortable.core.esm.js
+++ b/modular/sortable.core.esm.js
@@ -1461,7 +1461,7 @@ Sortable.prototype = /** @lends Sortable.prototype */{
}
target = parent; // store last element
}
- /* jshint boss:true */ while (parent = parent.parentNode);
+ /* jshint boss:true */ while (parent = parent.parentNode || parent.getRootNode().host);
}
_unhideGhostForTarget();
}
@@ -1781,11 +1781,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{
}
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) {
capture();
- if (elLastChild && elLastChild.nextSibling) {
- // the last draggable element is not the last node
- el.insertBefore(dragEl, elLastChild.nextSibling);
- } else {
- el.appendChild(dragEl);
+ try {
+ if (elLastChild && elLastChild.nextSibling) {
+ // the last draggable element is not the last node
+ el.insertBefore(dragEl, elLastChild.nextSibling);
+ } else {
+ el.appendChild(dragEl);
+ }
+ }
+ catch(err) {
+ return completed(false);
}
parentEl = el; // actualization
@@ -1802,7 +1807,13 @@ Sortable.prototype = /** @lends Sortable.prototype */{
targetRect = getRect(target);
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) {
capture();
- el.insertBefore(dragEl, firstChild);
+ try {
+ el.insertBefore(dragEl, firstChild);
+ }
+ catch(err) {
+ return completed(false);
+ }
+
parentEl = el; // actualization
changed();
@@ -1849,12 +1860,17 @@ Sortable.prototype = /** @lends Sortable.prototype */{
_silent = true;
setTimeout(_unsilent, 30);
capture();
- if (after && !nextSibling) {
- el.appendChild(dragEl);
- } else {
- target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
- }
+ try {
+ if (after && !nextSibling) {
+ el.appendChild(dragEl);
+ } else {
+ target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
+ }
+ }
+ catch(err) {
+ return completed(false);
+ }
// Undo chrome's scroll adjustment (has no effect on other browsers)
if (scrolledPastTop) {
scrollBy(scrolledPastTop, 0, scrollBefore - scrolledPastTop.scrollTop);

View File

@@ -10,6 +10,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types";
@@ -97,22 +98,25 @@ const DEVICES = [
},
];
const AREAS = [
const AREAS: AreaRegistryEntry[] = [
{
area_id: "backyard",
name: "Backyard",
icon: null,
picture: null,
aliases: [],
},
{
area_id: "bedroom",
name: "Bedroom",
icon: "mdi:bed",
picture: null,
aliases: [],
},
{
area_id: "livingroom",
name: "Livingroom",
icon: "mdi:sofa",
picture: null,
aliases: [],
},

View File

@@ -9,6 +9,7 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import "../../../../src/components/ha-selector/ha-selector";
import "../../../../src/components/ha-settings-row";
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
import { BlueprintInput } from "../../../../src/data/blueprint";
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
import { getEntity } from "../../../../src/fake_data/entity";
@@ -93,22 +94,25 @@ const DEVICES = [
},
];
const AREAS = [
const AREAS: AreaRegistryEntry[] = [
{
area_id: "backyard",
name: "Backyard",
icon: null,
picture: null,
aliases: [],
},
{
area_id: "bedroom",
name: "Bedroom",
icon: "mdi:bed",
picture: null,
aliases: [],
},
{
area_id: "livingroom",
name: "Livingroom",
icon: "mdi:sofa",
picture: null,
aliases: [],
},

View File

@@ -79,6 +79,18 @@ const CONFIGS = [
color: pink
`,
},
{
heading: "Whole tile tap action",
config: `
- type: tile
entity: switch.tv_outlet
color: pink
tap_action:
action: toggle
icon_tap_action:
action: none
`,
},
{
heading: "Unknown entity",
config: `

View File

@@ -255,7 +255,7 @@
"lit": "2.8.0",
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"sortablejs@1.15.0": "patch:sortablejs@npm%3A1.15.0#./.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.patch",
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
},
"packageManager": "yarn@4.0.2"

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20240125.0"
version = "20240131.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@@ -47,6 +47,12 @@ export class StateHistoryChartLine extends LitElement {
@property({ type: Boolean }) public logarithmicScale = false;
@property({ type: Number }) public minYAxis?: number;
@property({ type: Number }) public maxYAxis?: number;
@property({ type: Boolean }) public fitYData = false;
@state() private _chartData?: ChartData<"line">;
@state() private _entityIds: string[] = [];
@@ -84,7 +90,10 @@ export class StateHistoryChartLine extends LitElement {
changedProps.has("startTime") ||
changedProps.has("endTime") ||
changedProps.has("unit") ||
changedProps.has("logarithmicScale")
changedProps.has("logarithmicScale") ||
changedProps.has("minYAxis") ||
changedProps.has("maxYAxis") ||
changedProps.has("fitYData")
) {
this._chartOptions = {
parsing: false,
@@ -121,6 +130,10 @@ export class StateHistoryChartLine extends LitElement {
},
},
y: {
suggestedMin: this.fitYData ? this.minYAxis : null,
suggestedMax: this.fitYData ? this.maxYAxis : null,
min: this.fitYData ? null : this.minYAxis,
max: this.fitYData ? null : this.maxYAxis,
ticks: {
maxTicksLimit: 7,
},

View File

@@ -74,6 +74,12 @@ export class StateHistoryCharts extends LitElement {
@property({ type: Boolean }) public logarithmicScale = false;
@property({ type: Number }) public minYAxis?: number;
@property({ type: Number }) public maxYAxis?: number;
@property({ type: Boolean }) public fitYData = false;
private _computedStartTime!: Date;
private _computedEndTime!: Date;
@@ -161,6 +167,9 @@ export class StateHistoryCharts extends LitElement {
.chartIndex=${index}
.clickForMoreInfo=${this.clickForMoreInfo}
.logarithmicScale=${this.logarithmicScale}
.minYAxis=${this.minYAxis}
.maxYAxis=${this.maxYAxis}
.fitYData=${this.fitYData}
@y-width-changed=${this._yWidthChanged}
></state-history-chart-line>
</div> `;

View File

@@ -1,6 +1,6 @@
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
@@ -36,8 +36,12 @@ type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry;
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) =>
html`<ha-list-item
graphic="icon"
class=${classMap({ "add-new": item.area_id === "add_new" })}
>
${item.icon
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
: nothing}
${item.name}
</ha-list-item>`;
@@ -135,6 +139,7 @@ export class HaAreaPicker extends LitElement {
area_id: "no_areas",
name: this.hass.localize("ui.components.area-picker.no_areas"),
picture: null,
icon: null,
aliases: [],
},
];
@@ -262,7 +267,9 @@ export class HaAreaPicker extends LitElement {
}
if (areaIds) {
outputAreas = areas.filter((area) => areaIds!.includes(area.area_id));
outputAreas = outputAreas.filter((area) =>
areaIds!.includes(area.area_id)
);
}
if (excludeAreas) {
@@ -277,6 +284,7 @@ export class HaAreaPicker extends LitElement {
area_id: "no_areas",
name: this.hass.localize("ui.components.area-picker.no_match"),
picture: null,
icon: null,
aliases: [],
},
];
@@ -290,6 +298,7 @@ export class HaAreaPicker extends LitElement {
area_id: "add_new",
name: this.hass.localize("ui.components.area-picker.add_new"),
picture: null,
icon: "mdi:plus",
aliases: [],
},
];

View File

@@ -67,7 +67,8 @@ export class HaQrCode extends LitElement {
const computedStyles = getComputedStyle(this);
QRCode.toCanvas(canvas, this.data, {
errorCorrectionLevel: this.errorCorrectionLevel,
errorCorrectionLevel:
this.errorCorrectionLevel || (this.centerImage ? "Q" : "M"),
width: this.width,
scale: this.scale,
margin: this.margin,

View File

@@ -0,0 +1,30 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { QRCodeSelector } from "../../data/selector";
import "../ha-qr-code";
@customElement("ha-selector-qr_code")
export class HaSelectorQRCode extends LitElement {
@property({ attribute: false }) public selector!: QRCodeSelector;
protected render() {
return html`<ha-qr-code
.data=${this.selector.qr_code?.data}
.scale=${this.selector.qr_code?.scale}
.errorCorrectionLevel=${this.selector.qr_code?.error_correction_level}
.centerImage=${this.selector.qr_code?.center_image}
></ha-qr-code>`;
}
static styles = css`
ha-qr-code {
text-align: center;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-qr_code": HaSelectorQRCode;
}
}

View File

@@ -34,6 +34,7 @@ const LOAD_ELEMENTS = {
navigation: () => import("./ha-selector-navigation"),
number: () => import("./ha-selector-number"),
object: () => import("./ha-selector-object"),
qr_code: () => import("./ha-selector-qr-code"),
select: () => import("./ha-selector-select"),
selector: () => import("./ha-selector-selector"),
state: () => import("./ha-selector-state"),

View File

@@ -98,6 +98,7 @@ export class HaTargetPicker extends LitElement {
area_id,
area?.name || area_id,
undefined,
area?.icon,
mdiSofa
);
})
@@ -110,6 +111,7 @@ export class HaTargetPicker extends LitElement {
device_id,
device ? computeDeviceName(device, this.hass) : device_id,
undefined,
undefined,
mdiDevices
);
})
@@ -209,7 +211,8 @@ export class HaTargetPicker extends LitElement {
id: string,
name: string,
entityState?: HassEntity,
iconPath?: string
icon?: string | null,
fallbackIconPath?: string
) {
return html`
<div
@@ -217,12 +220,17 @@ export class HaTargetPicker extends LitElement {
[type]: true,
})}"
>
${iconPath
? html`<ha-svg-icon
${icon
? html`<ha-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.path=${iconPath}
></ha-svg-icon>`
: ""}
.icon=${icon}
></ha-icon>`
: fallbackIconPath
? html`<ha-svg-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.path=${fallbackIconPath}
></ha-svg-icon>`
: ""}
${entityState
? html`<ha-state-icon
class="mdc-chip__icon mdc-chip__icon--leading"

View File

@@ -133,7 +133,7 @@ export class HaLocationsEditor extends LitElement {
.layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom}
.autoFit=${this.autoFit}
.darkMode=${this.darkMode}
?darkMode=${this.darkMode}
></ha-map>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`

View File

@@ -156,9 +156,9 @@ export class HaMap extends ReactiveElement {
}
private _updateMapStyle(): void {
const darkMode = this.darkMode ?? this.hass.themes.darkMode ?? false;
const forcedDark = this.darkMode ?? false;
const map = this.shadowRoot!.getElementById("map");
const darkMode = this.darkMode || (this.hass.themes.darkMode ?? false);
const forcedDark = this.darkMode;
const map = this.renderRoot.querySelector("#map");
map!.classList.toggle("dark", darkMode);
map!.classList.toggle("forced-dark", forcedDark);
}
@@ -361,7 +361,7 @@ export class HaMap extends ReactiveElement {
);
const className =
this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light";
this.darkMode || this.hass.themes.darkMode ? "dark" : "light";
for (const entity of this.entities) {
const stateObj = hass.states[getEntityId(entity)];

View File

@@ -24,6 +24,8 @@ class HaUsersPickerLight extends LitElement {
@property({ attribute: false })
public users?: User[];
@property({ type: Boolean }) public disabled = false;
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (this.users === undefined) {
@@ -57,6 +59,7 @@ class HaUsersPickerLight extends LitElement {
this.users,
notSelectedUsers
)}
.disabled=${this.disabled}
@value-changed=${this._userChanged}
></ha-user-picker>
<ha-icon-button
@@ -78,7 +81,7 @@ class HaUsersPickerLight extends LitElement {
this.hass!.localize("ui.components.user-picker.add_user")}
.hass=${this.hass}
.users=${notSelectedUsers}
.disabled=${!notSelectedUsers?.length}
.disabled=${this.disabled || !notSelectedUsers?.length}
@value-changed=${this._addUser}
></ha-user-picker>
`;

View File

@@ -9,6 +9,7 @@ export interface AreaRegistryEntry {
area_id: string;
name: string;
picture: string | null;
icon: string | null;
aliases: string[];
}
@@ -23,6 +24,7 @@ export interface AreaDeviceLookup {
export interface AreaRegistryEntryMutableParams {
name: string;
picture?: string | null;
icon?: string | null;
aliases?: string[];
}

View File

@@ -74,8 +74,8 @@ export interface StateTrigger extends BaseTrigger {
platform: "state";
entity_id: string | string[];
attribute?: string;
from?: string | number;
to?: string | string[] | number;
from?: string | string[];
to?: string | string[];
for?: string | number | ForDict;
}

View File

@@ -199,57 +199,46 @@ const tryDescribeTrigger = (
// State Trigger
if (trigger.platform === "state") {
let base = "When";
const entities: string[] = [];
const states = hass.states;
let attribute = "";
if (trigger.attribute) {
const stateObj = Array.isArray(trigger.entity_id)
? hass.states[trigger.entity_id[0]]
: hass.states[trigger.entity_id];
base += ` ${computeAttributeNameDisplay(
attribute = computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
trigger.attribute
)} of`;
);
}
if (Array.isArray(trigger.entity_id)) {
for (const entity of trigger.entity_id.values()) {
const entityArray: string[] = ensureArray(trigger.entity_id);
if (entityArray) {
for (const entity of entityArray) {
if (states[entity]) {
entities.push(computeStateName(states[entity]) || entity);
}
}
} else if (trigger.entity_id) {
entities.push(
states[trigger.entity_id]
? computeStateName(states[trigger.entity_id])
: trigger.entity_id
);
}
if (entities.length === 0) {
// no entity_id or empty array
entities.push("something");
}
const stateObj = hass.states[entityArray[0]];
base += ` ${entities} changes`;
const stateObj =
hass.states[
Array.isArray(trigger.entity_id)
? trigger.entity_id[0]
: trigger.entity_id
];
let fromChoice = "other";
let fromString = "";
if (trigger.from !== undefined) {
let fromArray: string[] = [];
if (trigger.from === null) {
if (!trigger.attribute) {
base += " from any state";
fromChoice = "null";
}
} else if (Array.isArray(trigger.from)) {
} else {
fromArray = ensureArray(trigger.from);
const from: string[] = [];
for (const state of trigger.from.values()) {
for (const state of fromArray) {
from.push(
trigger.attribute
? hass
@@ -263,34 +252,25 @@ const tryDescribeTrigger = (
);
}
if (from.length !== 0) {
const fromString = formatListWithOrs(hass.locale, from);
base += ` from ${fromString}`;
fromString = formatListWithOrs(hass.locale, from);
fromChoice = "fromUsed";
}
} else {
base += ` from ${
trigger.attribute
? hass
.formatEntityAttributeValue(
stateObj,
trigger.attribute,
trigger.from
)
.toString()
: hass
.formatEntityState(stateObj, trigger.from.toString())
.toString()
}`;
}
}
let toChoice = "other";
let toString = "";
if (trigger.to !== undefined) {
let toArray: string[] = [];
if (trigger.to === null) {
if (!trigger.attribute) {
base += " to any state";
toChoice = "null";
}
} else if (Array.isArray(trigger.to)) {
} else {
toArray = ensureArray(trigger.to);
const to: string[] = [];
for (const state of trigger.to.values()) {
for (const state of toArray) {
to.push(
trigger.attribute
? hass
@@ -304,21 +284,9 @@ const tryDescribeTrigger = (
);
}
if (to.length !== 0) {
const toString = formatListWithOrs(hass.locale, to);
base += ` to ${toString}`;
toString = formatListWithOrs(hass.locale, to);
toChoice = "toUsed";
}
} else {
base += ` to ${
trigger.attribute
? hass
.formatEntityAttributeValue(
stateObj,
trigger.attribute,
trigger.to
)
.toString()
: hass.formatEntityState(stateObj, trigger.to.toString())
}`;
}
}
@@ -327,17 +295,29 @@ const tryDescribeTrigger = (
trigger.from === undefined &&
trigger.to === undefined
) {
base += " state or any attributes";
toChoice = "special";
}
let duration = "";
if (trigger.for) {
const duration = describeDuration(hass.locale, trigger.for);
if (duration) {
base += ` for ${duration}`;
}
duration = describeDuration(hass.locale, trigger.for) ?? "";
}
return base;
return hass.localize(
`${triggerTranslationBaseKey}.state.description.full`,
{
hasAttribute: attribute !== "" ? "true" : "false",
attribute: attribute,
hasEntity: entities.length !== 0 ? "true" : "false",
entity: formatListWithOrs(hass.locale, entities),
fromChoice: fromChoice,
fromString: fromString,
toChoice: toChoice,
toString: toString,
hasDuration: duration !== "" ? "true" : "false",
duration: duration,
}
);
}
// Sun Trigger

View File

@@ -72,6 +72,8 @@ export const enum ClimateEntityFeature {
PRESET_MODE = 16,
SWING_MODE = 32,
AUX_HEAT = 64,
TURN_OFF = 128,
TURN_ON = 256,
}
const hvacModeOrdering = HVAC_MODES.reduce(

View File

@@ -470,9 +470,15 @@ export const computeHistory = (
}[domain];
}
const deviceClass: string | undefined = (
currentState?.attributes || numericStateFromHistory?.a
)?.device_class;
const specialDomainClasses = {
climate: "temperature",
humidifier: "humidity",
water_heater: "temperature",
};
const deviceClass: string | undefined =
specialDomainClasses[domain] ||
(currentState?.attributes || numericStateFromHistory?.a)?.device_class;
const key = computeGroupKey(unit, deviceClass, splitDeviceClasses);

View File

@@ -3,6 +3,50 @@ import { navigate } from "../common/navigate";
import { HomeAssistant } from "../types";
import { subscribeDeviceRegistry } from "./device_registry";
export enum NetworkType {
THREAD = "thread",
WIFI = "wifi",
ETHERNET = "ethernet",
UNKNOWN = "unknown",
}
export enum NodeType {
END_DEVICE = "end_device",
SLEEPY_END_DEVICE = "sleepy_end_device",
ROUTING_END_DEVICE = "routing_end_device",
BRIDGE = "bridge",
UNKNOWN = "unknown",
}
export interface MatterFabricData {
fabric_id: number;
vendor_id: number;
fabric_index: number;
fabric_label?: string;
vendor_name?: string;
}
export interface MatterNodeDiagnostics {
node_id: number;
network_type: NetworkType;
node_type: NodeType;
network_name?: string;
ip_adresses: string[];
mac_address?: string;
available: boolean;
active_fabrics: MatterFabricData[];
}
export interface MatterPingResult {
[ip_address: string]: boolean;
}
export interface MatterCommissioningParameters {
setup_pin_code: number;
setup_manual_code: string;
setup_qr_code: string;
}
export const canCommissionMatterExternal = (hass: HomeAssistant) =>
hass.auth.external?.config.canCommissionMatter;
@@ -86,3 +130,50 @@ export const matterSetThread = (
type: "matter/set_thread",
thread_operation_dataset,
});
export const getMatterNodeDiagnostics = (
hass: HomeAssistant,
device_id: string
): Promise<MatterNodeDiagnostics> =>
hass.callWS({
type: "matter/node_diagnostics",
device_id,
});
export const pingMatterNode = (
hass: HomeAssistant,
device_id: string
): Promise<MatterPingResult> =>
hass.callWS({
type: "matter/ping_node",
device_id,
});
export const openMatterCommissioningWindow = (
hass: HomeAssistant,
device_id: string
): Promise<MatterCommissioningParameters> =>
hass.callWS({
type: "matter/open_commissioning_window",
device_id,
});
export const removeMatterFabric = (
hass: HomeAssistant,
device_id: string,
fabric_index: number
): Promise<void> =>
hass.callWS({
type: "matter/remove_matter_fabric",
device_id,
fabric_index,
});
export const interviewMatterNode = (
hass: HomeAssistant,
device_id: string
): Promise<void> =>
hass.callWS({
type: "matter/interview_node",
device_id,
});

View File

@@ -32,6 +32,13 @@ export const fetchRepairsIssues = (conn: Connection) =>
type: "repairs/list_issues",
});
export const fetchRepairsIssueData = (conn: Connection, domain, issue_id) =>
conn.sendMessagePromise<{ issue_data: { string: any } }>({
type: "repairs/get_issue_data",
domain,
issue_id,
});
export const ignoreRepairsIssue = async (
hass: HomeAssistant,
issue: RepairsIssue,

View File

@@ -41,6 +41,7 @@ export type Selector =
| NumberSelector
| ObjectSelector
| AssistPipelineSelector
| QRCodeSelector
| SelectSelector
| SelectorSelector
| StateSelector
@@ -340,6 +341,15 @@ export interface BackupLocationSelector {
backup_location: {} | null;
}
export interface QRCodeSelector {
qr_code: {
data: string;
scale?: number;
error_correction_level?: "low" | "medium" | "quartile" | "high";
center_image?: string;
} | null;
}
export interface StringSelector {
text: {
multiline?: boolean;

View File

@@ -22,6 +22,7 @@ export interface ThreadDataSet {
network_name: string;
pan_id: string | null;
preferred_border_agent_id: string | null;
preferred_extended_address: string | null;
preferred: boolean;
source: string;
}
@@ -107,10 +108,12 @@ export const setPreferredThreadDataSet = (
export const setPreferredBorderAgent = (
hass: HomeAssistant,
dataset_id: string,
border_agent_id: string
border_agent_id: string | null,
extended_address: string
): Promise<void> =>
hass.callWS({
type: "thread/set_preferred_border_agent_id",
type: "thread/set_preferred_border_agent",
dataset_id,
border_agent_id,
extended_address,
});

View File

@@ -1,19 +1,12 @@
import { Connection, createCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
import { stringCompare } from "../common/string/compare";
import { AreaRegistryEntry } from "./area_registry";
import { debounce } from "../common/util/debounce";
import { AreaRegistryEntry } from "./area_registry";
const fetchAreaRegistry = (conn: Connection) =>
conn
.sendMessagePromise({
type: "config/area_registry/list",
})
.then((areas) =>
(areas as AreaRegistryEntry[]).sort((ent1, ent2) =>
stringCompare(ent1.name, ent2.name)
)
);
conn.sendMessagePromise<AreaRegistryEntry[]>({
type: "config/area_registry/list",
});
const subscribeAreaRegistryUpdates = (
conn: Connection,

View File

@@ -73,7 +73,7 @@ export interface ClusterAttributeData {
export interface AttributeConfigurationStatus {
id: number;
name: string;
success: boolean | undefined;
status: string;
min: number;
max: number;
change: number;

View File

@@ -35,6 +35,13 @@ interface EMOutgoingMessageConfigGet extends EMMessage {
type: "config/get";
}
interface EMOutgoingMessageScanQRCode extends EMMessage {
type: "qr_code/scan";
title: string;
description: string;
alternative_option_label?: string;
}
interface EMOutgoingMessageMatterCommission extends EMMessage {
type: "matter/commission";
}
@@ -48,6 +55,13 @@ type EMOutgoingMessageWithAnswer = {
request: EMOutgoingMessageConfigGet;
response: ExternalConfig;
};
"qr_code/scan": {
request: EMOutgoingMessageScanQRCode;
response:
| EMIncomingMessageQRCodeResponseCanceled
| EMIncomingMessageQRCodeResponseAlternativeOptions
| EMIncomingMessageQRCodeResponseScanResult;
};
};
interface EMOutgoingMessageExoplayerPlayHLS extends EMMessage {
@@ -158,6 +172,19 @@ interface EMIncomingMessageShowAutomationEditor {
};
}
export interface EMIncomingMessageQRCodeResponseCanceled {
action: "canceled";
}
export interface EMIncomingMessageQRCodeResponseAlternativeOptions {
action: "alternative_options";
}
export interface EMIncomingMessageQRCodeResponseScanResult {
action: "scan_result";
result: string;
}
export type EMIncomingMessageCommands =
| EMIncomingMessageRestart
| EMIncomingMessageShowNotifications
@@ -180,6 +207,7 @@ export interface ExternalConfig {
canCommissionMatter: boolean;
canImportThreadCredentials: boolean;
hasAssist: boolean;
hasQRScanner: number;
}
export class ExternalMessaging {

View File

@@ -9,6 +9,7 @@ import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-picture-upload";
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
import "../../../components/ha-settings-row";
import "../../../components/ha-icon-picker";
import "../../../components/ha-textfield";
import { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
@@ -32,6 +33,8 @@ class DialogAreaDetail extends LitElement {
@state() private _picture!: string | null;
@state() private _icon!: string | null;
@state() private _error?: string;
@state() private _params?: AreaRegistryDetailDialogParams;
@@ -46,6 +49,7 @@ class DialogAreaDetail extends LitElement {
this._name = this._params.entry ? this._params.entry.name : "";
this._aliases = this._params.entry ? this._params.entry.aliases : [];
this._picture = this._params.entry?.picture || null;
this._icon = this._params.entry?.icon || null;
await this.updateComplete;
}
@@ -101,6 +105,13 @@ class DialogAreaDetail extends LitElement {
dialogInitialFocus
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
.value=${this._icon}
@value-changed=${this._iconChanged}
.label=${this.hass.localize("ui.panel.config.areas.editor.icon")}
></ha-icon-picker>
<ha-picture-upload
.hass=${this.hass}
.value=${this._picture}
@@ -152,23 +163,30 @@ class DialogAreaDetail extends LitElement {
this._name = ev.target.value;
}
private _iconChanged(ev) {
this._error = undefined;
this._icon = ev.detail.value;
}
private _pictureChanged(ev: ValueChangedEvent<string | null>) {
this._error = undefined;
this._picture = (ev.target as HaPictureUpload).value;
}
private async _updateEntry() {
const create = !this._params!.entry;
this._submitting = true;
try {
const values: AreaRegistryEntryMutableParams = {
name: this._name.trim(),
picture: this._picture,
picture: this._picture || (create ? undefined : null),
icon: this._icon || (create ? undefined : null),
aliases: this._aliases,
};
if (this._params!.entry) {
await this._params!.updateEntry!(values);
} else {
if (create) {
await this._params!.createEntry!(values);
} else {
await this._params!.updateEntry!(values);
}
this.closeDialog();
} catch (err: any) {
@@ -189,6 +207,7 @@ class DialogAreaDetail extends LitElement {
haStyleDialog,
css`
ha-textfield,
ha-icon-picker,
ha-picture-upload {
display: block;
margin-bottom: 16px;

View File

@@ -1,11 +1,9 @@
import { consume } from "@lit-labs/context";
import "@material/mwc-button";
import "@material/mwc-list";
import { mdiDelete, mdiDotsVertical, mdiImagePlus, mdiPencil } from "@mdi/js";
import {
HassEntity,
UnsubscribeFunc,
} from "home-assistant-js-websocket/dist/types";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { HassEntity } from "home-assistant-js-websocket/dist/types";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
@@ -18,33 +16,31 @@ import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-list-item";
import {
AreaRegistryEntry,
deleteAreaRegistryEntry,
subscribeAreaRegistry,
updateAreaRegistryEntry,
} from "../../../data/area_registry";
import { AutomationEntity } from "../../../data/automation";
import { fullEntitiesContext } from "../../../data/context";
import {
computeDeviceName,
DeviceRegistryEntry,
computeDeviceName,
sortDeviceRegistryByName,
subscribeDeviceRegistry,
} from "../../../data/device_registry";
import {
computeEntityRegistryName,
EntityRegistryEntry,
computeEntityRegistryName,
sortEntityRegistryByName,
subscribeEntityRegistry,
} from "../../../data/entity_registry";
import { SceneEntity } from "../../../data/scene";
import { ScriptEntity } from "../../../data/script";
import { findRelated, RelatedResult } from "../../../data/search";
import { RelatedResult, findRelated } from "../../../data/search";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../../logbook/ha-logbook";
@@ -52,7 +48,6 @@ import {
loadAreaRegistryDetailDialog,
showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail";
import "../../../components/ha-list-item";
declare type NameAndEntity<EntityType extends HassEntity> = {
name: string;
@@ -60,7 +55,7 @@ declare type NameAndEntity<EntityType extends HassEntity> = {
};
@customElement("ha-config-area-page")
class HaConfigAreaPage extends SubscribeMixin(LitElement) {
class HaConfigAreaPage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public areaId!: string;
@@ -71,24 +66,14 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public showAdvanced = false;
@state() public _areas!: AreaRegistryEntry[];
@state() public _devices!: DeviceRegistryEntry[];
@state() public _entities!: EntityRegistryEntry[];
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@state() private _related?: RelatedResult;
private _logbookTime = { recent: 86400 };
private _area = memoizeOne(
(
areaId: string,
areas: AreaRegistryEntry[]
): AreaRegistryEntry | undefined =>
areas.find((area) => area.area_id === areaId)
);
private _memberships = memoizeOne(
(
areaId: string,
@@ -150,26 +135,12 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
}
}
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeAreaRegistry(this.hass.connection, (areas) => {
this._areas = areas;
}),
subscribeDeviceRegistry(this.hass.connection, (entries) => {
this._devices = entries;
}),
subscribeEntityRegistry(this.hass.connection, (entries) => {
this._entities = entries;
}),
];
}
protected render() {
if (!this._areas || !this._devices || !this._entities) {
if (!this.hass.areas || !this.hass.devices || !this.hass.entities) {
return nothing;
}
const area = this._area(this.areaId, this._areas);
const area = this.hass.areas[this.areaId];
if (!area) {
return html`
@@ -182,8 +153,8 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
const memberships = this._memberships(
this.areaId,
this._devices,
this._entities
Object.values(this.hass.devices),
this._entityReg
);
const { devices, entities } = memberships;
@@ -617,7 +588,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
}
private _renderScript(name: string, entityState: ScriptEntity) {
const entry = this._entities.find(
const entry = this._entityReg.find(
(e) => e.entity_id === entityState.entity_id
);
let url = `/config/script/show/${entityState.entity_id}`;
@@ -657,7 +628,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
}
private async _deleteConfirm() {
const area = this._area(this.areaId, this._areas);
const area = this.hass.areas[this.areaId];
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.areas.delete.confirmation_title",
@@ -686,7 +657,6 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
font-weight: 500;
color: var(--secondary-text-color);
}
img {
border-radius: var(--ha-card-border-radius, 12px);
width: 100%;

View File

@@ -1,7 +1,13 @@
import { mdiHelpCircle, mdiPlus } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
CSSResultGroup,
LitElement,
TemplateResult,
css,
html,
nothing,
} from "lit";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { formatListWithAnds } from "../../../common/string/format-list";
@@ -11,19 +17,9 @@ import "../../../components/ha-svg-icon";
import {
AreaRegistryEntry,
createAreaRegistryEntry,
subscribeAreaRegistry,
} from "../../../data/area_registry";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../data/entity_registry";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../../types";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
@@ -33,7 +29,7 @@ import {
} from "./show-dialog-area-registry-detail";
@customElement("ha-config-areas-dashboard")
export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
export class HaConfigAreasDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide = false;
@@ -42,24 +38,18 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public route!: Route;
@state() private _areas!: AreaRegistryEntry[];
@state() private _devices!: DeviceRegistryEntry[];
@state() private _entities!: EntityRegistryEntry[];
private _processAreas = memoizeOne(
(
areas: AreaRegistryEntry[],
devices: DeviceRegistryEntry[],
entities: EntityRegistryEntry[]
) =>
areas.map((area) => {
areas: HomeAssistant["areas"],
devices: HomeAssistant["devices"],
entities: HomeAssistant["entities"]
) => {
const processArea = (area: AreaRegistryEntry) => {
let noDevicesInArea = 0;
let noServicesInArea = 0;
let noEntitiesInArea = 0;
for (const device of devices) {
for (const device of Object.values(devices)) {
if (device.area_id === area.area_id) {
if (device.entry_type === "service") {
noServicesInArea++;
@@ -69,7 +59,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
}
}
for (const entity of entities) {
for (const entity of Object.values(entities)) {
if (entity.area_id === area.area_id) {
noEntitiesInArea++;
}
@@ -81,24 +71,22 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
services: noServicesInArea,
entities: noEntitiesInArea,
};
})
};
return Object.values(areas).map(processArea);
}
);
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeAreaRegistry(this.hass.connection, (areas) => {
this._areas = areas;
}),
subscribeDeviceRegistry(this.hass.connection, (entries) => {
this._devices = entries;
}),
subscribeEntityRegistry(this.hass.connection, (entries) => {
this._entities = entries;
}),
];
}
protected render(): TemplateResult {
const areas =
!this.hass.areas || !this.hass.devices || !this.hass.entities
? undefined
: this._processAreas(
this.hass.areas,
this.hass.devices,
this.hass.entities
);
return html`
<hass-tabs-subpage
.hass=${this.hass}
@@ -115,52 +103,11 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
@click=${this._showHelp}
></ha-icon-button>
<div class="container">
${!this._areas || !this._devices || !this._entities
? ""
: this._processAreas(
this._areas,
this._devices,
this._entities
).map(
(area) =>
html`<a href=${`/config/areas/area/${area.area_id}`}
><ha-card outlined>
<div
style=${styleMap({
backgroundImage: area.picture
? `url(${area.picture})`
: undefined,
})}
class="picture ${!area.picture ? "placeholder" : ""}"
></div>
<h1 class="card-header">${area.name}</h1>
<div class="card-content">
<div>
${formatListWithAnds(
this.hass.locale,
[
area.devices &&
this.hass.localize(
"ui.panel.config.integrations.config_entry.devices",
{ count: area.devices }
),
area.services &&
this.hass.localize(
"ui.panel.config.integrations.config_entry.services",
{ count: area.services }
),
area.entities &&
this.hass.localize(
"ui.panel.config.integrations.config_entry.entities",
{ count: area.entities }
),
].filter((v): v is string => Boolean(v))
)}
</div>
</div>
</ha-card></a
>`
)}
${areas?.length
? html`<div class="areas">
${areas.map((area) => this._renderArea(area))}
</div>`
: nothing}
</div>
<ha-fab
slot="fab"
@@ -176,13 +123,55 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
`;
}
private _renderArea(area) {
return html`<a href=${`/config/areas/area/${area.area_id}`}>
<ha-card outlined>
<div
style=${styleMap({
backgroundImage: area.picture ? `url(${area.picture})` : undefined,
})}
class="picture ${!area.picture ? "placeholder" : ""}"
>
${!area.picture && area.icon
? html`<ha-icon .icon=${area.icon}></ha-icon>`
: ""}
</div>
<h1 class="card-header">${area.name}</h1>
<div class="card-content">
<div>
${formatListWithAnds(
this.hass.locale,
[
area.devices &&
this.hass.localize(
"ui.panel.config.integrations.config_entry.devices",
{ count: area.devices }
),
area.services &&
this.hass.localize(
"ui.panel.config.integrations.config_entry.services",
{ count: area.services }
),
area.entities &&
this.hass.localize(
"ui.panel.config.integrations.config_entry.entities",
{ count: area.entities }
),
].filter((v): v is string => Boolean(v))
)}
</div>
</div>
</ha-card>
</a>`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
loadAreaRegistryDetailDialog();
}
private _createArea() {
this._openDialog();
this._openAreaDialog();
}
private _showHelp() {
@@ -202,7 +191,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
});
}
private _openDialog(entry?: AreaRegistryEntry) {
private _openAreaDialog(entry?: AreaRegistryEntry) {
showAreaRegistryDetailDialog(this, {
entry,
createEntry: async (values) =>
@@ -213,14 +202,17 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
static get styles(): CSSResultGroup {
return css`
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 16px 16px;
padding: 8px 16px 16px;
margin: 0 auto 64px auto;
max-width: 2000px;
}
.container > * {
.areas {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-gap: 16px 16px;
max-width: 2000px;
margin-bottom: 16px;
}
.areas > * {
max-width: 500px;
}
ha-card {
@@ -239,6 +231,12 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
background-position: center;
position: relative;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
--mdc-icon-size: 48px;
}
.picture.placeholder::before {
position: absolute;
content: "";

View File

@@ -77,7 +77,7 @@ export default class HaAutomationAction extends LitElement {
return html`
<ha-sortable
handle-selector=".handle"
.disabled=${!this._showReorder}
.disabled=${!this._showReorder || this.disabled}
@item-moved=${this._actionMoved}
group="actions"
.path=${this.path}
@@ -101,7 +101,7 @@ export default class HaAutomationAction extends LitElement {
@value-changed=${this._actionChanged}
.hass=${this.hass}
>
${this._showReorder
${this._showReorder && !this.disabled
? html`
<div class="handle" slot="icons">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>

View File

@@ -122,7 +122,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
return html`
<ha-sortable
handle-selector=".handle"
.disabled=${!this._showReorder}
.disabled=${!this._showReorder || this.disabled}
group="choose-options"
.path=${[...(this.path ?? []), "choose"]}
>
@@ -148,7 +148,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
? ""
: this._getDescription(option))}
</h3>
${this._showReorder
${this._showReorder && !this.disabled
? html`
<div class="handle" slot="icons">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>

View File

@@ -117,7 +117,7 @@ export default class HaAutomationCondition extends LitElement {
return html`
<ha-sortable
handle-selector=".handle"
.disabled=${!this._showReorder}
.disabled=${!this._showReorder || this.disabled}
@item-moved=${this._conditionMoved}
group="conditions"
.path=${this.path}
@@ -141,7 +141,7 @@ export default class HaAutomationCondition extends LitElement {
@value-changed=${this._conditionChanged}
.hass=${this.hass}
>
${this._showReorder
${this._showReorder && !this.disabled
? html`
<div class="handle" slot="icons">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>

View File

@@ -74,7 +74,7 @@ export default class HaAutomationTrigger extends LitElement {
return html`
<ha-sortable
handle-selector=".handle"
.disabled=${!this._showReorder}
.disabled=${!this._showReorder || this.disabled}
@item-moved=${this._triggerMoved}
group="triggers"
.path=${this.path}
@@ -97,7 +97,7 @@ export default class HaAutomationTrigger extends LitElement {
.hass=${this.hass}
.disabled=${this.disabled}
>
${this._showReorder
${this._showReorder && !this.disabled
? html`
<div class="handle" slot="icons">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>

View File

@@ -0,0 +1,88 @@
import {
mdiAccessPoint,
mdiChatProcessing,
mdiChatQuestion,
mdiExportVariant,
} from "@mdi/js";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import {
NetworkType,
getMatterNodeDiagnostics,
} from "../../../../../../data/matter";
import type { HomeAssistant } from "../../../../../../types";
import { showMatterReinterviewNodeDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-reinterview-node";
import { showMatterPingNodeDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-ping-node";
import { showMatterOpenCommissioningWindowDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-open-commissioning-window";
import type { DeviceAction } from "../../../ha-config-device-page";
import { showMatterManageFabricsDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-manage-fabrics";
import { navigate } from "../../../../../../common/navigate";
export const getMatterDeviceActions = async (
el: HTMLElement,
hass: HomeAssistant,
device: DeviceRegistryEntry
): Promise<DeviceAction[]> => {
if (device.via_device_id !== null) {
// only show device actions for top level nodes (so not bridged)
return [];
}
const nodeDiagnostics = await getMatterNodeDiagnostics(hass, device.id);
const actions: DeviceAction[] = [];
if (nodeDiagnostics.available) {
// actions that can only be performed if the device is alive
actions.push({
label: hass.localize(
"ui.panel.config.matter.device_actions.open_commissioning_window"
),
icon: mdiExportVariant,
action: () =>
showMatterOpenCommissioningWindowDialog(el, {
device_id: device.id,
}),
});
actions.push({
label: hass.localize(
"ui.panel.config.matter.device_actions.manage_fabrics"
),
icon: mdiExportVariant,
action: () =>
showMatterManageFabricsDialog(el, {
device_id: device.id,
}),
});
actions.push({
label: hass.localize(
"ui.panel.config.matter.device_actions.reinterview_device"
),
icon: mdiChatProcessing,
action: () =>
showMatterReinterviewNodeDialog(el, {
device_id: device.id,
}),
});
}
if (nodeDiagnostics.network_type === NetworkType.THREAD) {
actions.push({
label: hass.localize(
"ui.panel.config.matter.device_actions.view_thread_network"
),
icon: mdiAccessPoint,
action: () => navigate("/config/thread"),
});
}
actions.push({
label: hass.localize("ui.panel.config.matter.device_actions.ping_device"),
icon: mdiChatQuestion,
action: () =>
showMatterPingNodeDialog(el, {
device_id: device.id,
}),
});
return actions;
};

View File

@@ -0,0 +1,174 @@
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../../components/ha-expansion-panel";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import {
getMatterNodeDiagnostics,
MatterNodeDiagnostics,
} from "../../../../../../data/matter";
import "@material/mwc-list";
import "../../../../../../components/ha-list-item";
import { SubscribeMixin } from "../../../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
@customElement("ha-device-info-matter")
export class HaDeviceInfoMatter extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public device!: DeviceRegistryEntry;
@state() private _nodeDiagnostics?: MatterNodeDiagnostics;
public willUpdate(changedProperties: PropertyValues) {
super.willUpdate(changedProperties);
if (changedProperties.has("device")) {
this._fetchNodeDetails();
}
}
private async _fetchNodeDetails() {
if (!this.device) {
return;
}
if (this.device.via_device_id !== null) {
// only show device details for top level nodes (so not bridged)
return;
}
try {
this._nodeDiagnostics = await getMatterNodeDiagnostics(
this.hass,
this.device.id
);
} catch (err: any) {
this._nodeDiagnostics = undefined;
}
}
protected render() {
if (!this._nodeDiagnostics) {
return nothing;
}
return html`
<ha-expansion-panel
.header=${this.hass.localize(
"ui.panel.config.matter.device_info.device_info"
)}
>
<div class="row">
<span class="name"
>${this.hass.localize(
"ui.panel.config.matter.device_info.node_id"
)}:</span
>
<span class="value">${this._nodeDiagnostics.node_id}</span>
</div>
<div class="row">
<span class="name"
>${this.hass.localize(
"ui.panel.config.matter.device_info.network_type"
)}:</span
>
<span class="value"
>${this.hass.localize(
`ui.panel.config.matter.network_type.${this._nodeDiagnostics.network_type}`
)}</span
>
</div>
<div class="row">
<span class="name"
>${this.hass.localize(
"ui.panel.config.matter.device_info.node_type"
)}:</span
>
<span class="value"
>${this.hass.localize(
`ui.panel.config.matter.node_type.${this._nodeDiagnostics.node_type}`
)}</span
>
</div>
${this._nodeDiagnostics.network_name
? html`
<div class="row">
<span class="name"
>${this.hass.localize(
"ui.panel.config.matter.device_info.network_name"
)}:</span
>
<span class="value">${this._nodeDiagnostics.network_name}</span>
</div>
`
: nothing}
${this._nodeDiagnostics.mac_address
? html`
<div class="row">
<span class="name"
>${this.hass.localize(
"ui.panel.config.matter.device_info.mac_address"
)}:</span
>
<span class="value">${this._nodeDiagnostics.mac_address}</span>
</div>
`
: nothing}
<div class="row">
<span class="name"
>${this.hass.localize(
"ui.panel.config.matter.device_info.ip_adresses"
)}:</span
>
<span class="value"
>${this._nodeDiagnostics.ip_adresses.map(
(ip) => html`${ip}<br />`
)}</span
>
</div>
</ha-expansion-panel>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
h4 {
margin-bottom: 4px;
}
div {
word-break: break-all;
margin-top: 2px;
}
.row {
display: flex;
justify-content: space-between;
padding-bottom: 4px;
}
.value {
text-align: right;
}
ha-expansion-panel {
margin: 8px -16px 0;
--expansion-panel-summary-padding: 0 16px;
--expansion-panel-content-padding: 0 16px;
--ha-card-border-radius: 0px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-device-info-matter": HaDeviceInfoMatter;
}
}

View File

@@ -1099,6 +1099,17 @@ export class HaConfigDevicePage extends LitElement {
);
deviceActions.push(...actions);
}
if (domains.includes("matter")) {
const matter = await import(
"./device-detail/integration-elements/matter/device-actions"
);
const actions = await matter.getMatterDeviceActions(
this,
this.hass,
device
);
deviceActions.push(...actions);
}
this._deviceActions = deviceActions;
}
@@ -1204,6 +1215,17 @@ export class HaConfigDevicePage extends LitElement {
></ha-device-info-zwave_js>
`);
}
if (domains.includes("matter")) {
import(
"./device-detail/integration-elements/matter/ha-device-info-matter"
);
deviceInfo.push(html`
<ha-device-info-matter
.hass=${this.hass}
.device=${device}
></ha-device-info-matter>
`);
}
}
private async _showSettings() {

View File

@@ -0,0 +1,169 @@
import "@material/mwc-button/mwc-button";
import { mdiClose } from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import "../../../../../components/ha-qr-code";
import {
MatterFabricData,
MatterNodeDiagnostics,
getMatterNodeDiagnostics,
removeMatterFabric,
} from "../../../../../data/matter";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../../dialogs/generic/show-dialog-box";
import { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { MatterManageFabricsDialogParams } from "./show-dialog-matter-manage-fabrics";
const NABUCASA_FABRIC = 4939;
@customElement("dialog-matter-manage-fabrics")
class DialogMatterManageFabrics extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private device_id?: string;
@state() private _nodeDiagnostics?: MatterNodeDiagnostics;
public async showDialog(
params: MatterManageFabricsDialogParams
): Promise<void> {
this.device_id = params.device_id;
this._fetchNodeDetails();
}
protected render() {
if (!this.device_id) {
return nothing;
}
return html`
<ha-dialog
open
hideActions
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.matter.manage_fabrics.title")
)}
>
<p>
${this.hass.localize("ui.panel.config.matter.manage_fabrics.fabrics")}
</p>
${this._nodeDiagnostics
? html`<mwc-list>
${this._nodeDiagnostics.active_fabrics.map(
(fabric) =>
html`<ha-list-item
noninteractive
.hasMeta=${this._nodeDiagnostics?.available &&
fabric.vendor_id !== NABUCASA_FABRIC}
>${fabric.vendor_name ||
fabric.fabric_label ||
fabric.vendor_id}
<ha-icon-button
@click=${this._removeFabric}
slot="meta"
.fabric=${fabric}
.path=${mdiClose}
></ha-icon-button>
</ha-list-item>`
)}
</mwc-list>`
: html`<div class="center">
<ha-circular-progress indeterminate></ha-circular-progress>
</div>`}
</ha-dialog>
`;
}
private async _fetchNodeDetails() {
if (!this.device_id) {
return;
}
try {
this._nodeDiagnostics = await getMatterNodeDiagnostics(
this.hass,
this.device_id
);
} catch (err: any) {
this._nodeDiagnostics = undefined;
}
}
private async _removeFabric(ev) {
const fabric: MatterFabricData = ev.target.fabric;
const fabricName =
fabric.vendor_name || fabric.fabric_label || fabric.vendor_id.toString();
const confirm = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.matter.manage_fabrics.remove_fabric_confirm_header",
{ fabric: fabricName }
),
text: this.hass.localize(
"ui.panel.config.matter.manage_fabrics.remove_fabric_confirm_text",
{ fabric: fabricName }
),
warning: true,
});
if (!confirm) {
return;
}
try {
await removeMatterFabric(this.hass, this.device_id!, fabric.fabric_index);
this._fetchNodeDetails();
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.matter.manage_fabrics.remove_fabric_failed_header",
{ fabric: fabricName }
),
text: this.hass.localize(
"ui.panel.config.matter.manage_fabrics.remove_fabric_failed_text"
),
});
}
}
public closeDialog(): void {
this.device_id = undefined;
this._nodeDiagnostics = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--dialog-content-padding: 0;
--mdc-list-side-padding: 24px;
--mdc-list-side-padding-right: 16px;
--mdc-list-item-meta-size: 48px;
}
p {
margin: 8px 24px;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-matter-manage-fabrics": DialogMatterManageFabrics;
}
}

View File

@@ -0,0 +1,200 @@
import "@material/mwc-button/mwc-button";
import { mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-circular-progress";
import "../../../../../components/ha-qr-code";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import {
openMatterCommissioningWindow,
MatterCommissioningParameters,
} from "../../../../../data/matter";
import { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { MatterOpenCommissioningWindowDialogParams } from "./show-dialog-matter-open-commissioning-window";
@customElement("dialog-matter-open-commissioning-window")
class DialogMatterOpenCommissioningWindow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private device_id?: string;
@state() private _status?: string;
@state() private _commissionParams?: MatterCommissioningParameters;
public async showDialog(
params: MatterOpenCommissioningWindowDialogParams
): Promise<void> {
this.device_id = params.device_id;
}
protected render() {
if (!this.device_id) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
"ui.panel.config.matter.open_commissioning_window.title"
)
)}
>
${this._commissionParams
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.matter.open_commissioning_window.sharing_code"
)}: <b>${this._commissionParams.setup_manual_code}</b>
</p>
</div>
</div>
<ha-qr-code
.data=${this._commissionParams.setup_qr_code}
errorCorrectionLevel="quartile"
scale="6"
></ha-qr-code>
<div></div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "started"
? html`
<div class="flex-container">
<ha-circular-progress indeterminate></ha-circular-progress>
<div class="status">
<p>
<b>
${this.hass.localize(
"ui.panel.config.matter.open_commissioning_window.in_progress"
)}
</b>
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "failed"
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCloseCircle}
class="failed"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.matter.open_commissioning_window.failed"
)}
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: html`
<p>
${this.hass.localize(
"ui.panel.config.matter.open_commissioning_window.introduction"
)}
</p>
<mwc-button slot="primaryAction" @click=${this._start}>
${this.hass.localize(
"ui.panel.config.matter.open_commissioning_window.start_commissioning"
)}
</mwc-button>
`}
</ha-dialog>
`;
}
private async _start(): Promise<void> {
if (!this.hass) {
return;
}
this._status = "started";
this._commissionParams = undefined;
try {
this._commissionParams = await openMatterCommissioningWindow(
this.hass,
this.device_id!
);
} catch (e) {
this._status = "failed";
}
}
public closeDialog(): void {
this.device_id = undefined;
this._status = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
.success {
color: var(--success-color);
}
.failed {
color: var(--error-color);
}
.flex-container {
display: flex;
align-items: center;
}
.stages {
margin-top: 16px;
}
.stage ha-svg-icon {
width: 16px;
height: 16px;
}
.stage {
padding: 8px;
}
ha-svg-icon {
width: 68px;
height: 48px;
}
ha-qr-code {
text-align: center;
}
.flex-container ha-circular-progress,
.flex-container ha-svg-icon {
margin-right: 20px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-matter-open-commissioning-window": DialogMatterOpenCommissioningWindow;
}
}

View File

@@ -0,0 +1,199 @@
import "@material/mwc-button/mwc-button";
import { mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import { pingMatterNode, MatterPingResult } from "../../../../../data/matter";
import { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { MatterPingNodeDialogParams } from "./show-dialog-matter-ping-node";
@customElement("dialog-matter-ping-node")
class DialogMatterPingNode extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private device_id?: string;
@state() private _status?: string;
@state() private _pingResult?: MatterPingResult;
public async showDialog(params: MatterPingNodeDialogParams): Promise<void> {
this.device_id = params.device_id;
}
protected render() {
if (!this.device_id) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.matter.ping_node.title")
)}
>
${this._pingResult
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.matter.ping_node.ping_complete"
)}
</p>
</div>
</div>
<div>
<mwc-list>
${Object.entries(this._pingResult).map(
([ip, success]) =>
html`<ha-list-item hasMeta
>${ip}
<ha-icon
slot="meta"
icon=${success ? "mdi:check" : "mdi:close"}
></ha-icon>
</ha-list-item>`
)}
</mwc-list>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "started"
? html`
<div class="flex-container">
<ha-circular-progress indeterminate></ha-circular-progress>
<div class="status">
<p>
<b>
${this.hass.localize(
"ui.panel.config.matter.ping_node.in_progress"
)}
</b>
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "failed"
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCloseCircle}
class="failed"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.matter.ping_node.ping_failed"
)}
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: html`
<p>
${this.hass.localize(
"ui.panel.config.matter.ping_node.introduction"
)}
</p>
<p>
<em>
${this.hass.localize(
"ui.panel.config.matter.ping_node.battery_device_warning"
)}
</em>
</p>
<mwc-button slot="primaryAction" @click=${this._startPing}>
${this.hass.localize(
"ui.panel.config.matter.ping_node.start_ping"
)}
</mwc-button>
`}
</ha-dialog>
`;
}
private async _startPing(): Promise<void> {
if (!this.hass) {
return;
}
this._status = "started";
try {
this._pingResult = await pingMatterNode(this.hass, this.device_id!);
} catch (err) {
this._status = "failed";
}
}
public closeDialog(): void {
this.device_id = undefined;
this._status = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
.success {
color: var(--success-color);
}
.failed {
color: var(--error-color);
}
.flex-container {
display: flex;
align-items: center;
}
.stages {
margin-top: 16px;
}
.stage ha-svg-icon {
width: 16px;
height: 16px;
}
.stage {
padding: 8px;
}
ha-svg-icon {
width: 68px;
height: 48px;
}
.flex-container ha-circular-progress,
.flex-container ha-svg-icon {
margin-right: 20px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-matter-ping-node": DialogMatterPingNode;
}
}

View File

@@ -0,0 +1,193 @@
import "@material/mwc-button/mwc-button";
import { mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import { interviewMatterNode } from "../../../../../data/matter";
import { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { MatterReinterviewNodeDialogParams } from "./show-dialog-matter-reinterview-node";
@customElement("dialog-matter-reinterview-node")
class DialogMatterReinterviewNode extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private device_id?: string;
@state() private _status?: string;
public async showDialog(
params: MatterReinterviewNodeDialogParams
): Promise<void> {
this.device_id = params.device_id;
}
protected render() {
if (!this.device_id) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.matter.reinterview_node.title")
)}
>
${!this._status
? html`
<p>
${this.hass.localize(
"ui.panel.config.matter.reinterview_node.introduction"
)}
</p>
<p>
<em>
${this.hass.localize(
"ui.panel.config.matter.reinterview_node.battery_device_warning"
)}
</em>
</p>
<mwc-button slot="primaryAction" @click=${this._startReinterview}>
${this.hass.localize(
"ui.panel.config.matter.reinterview_node.start_reinterview"
)}
</mwc-button>
`
: this._status === "started"
? html`
<div class="flex-container">
<ha-circular-progress indeterminate></ha-circular-progress>
<div class="status">
<p>
<b>
${this.hass.localize(
"ui.panel.config.matter.reinterview_node.in_progress"
)}
</b>
</p>
<p>
${this.hass.localize(
"ui.panel.config.matter.reinterview_node.run_in_background"
)}
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "failed"
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCloseCircle}
class="failed"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.matter.reinterview_node.interview_failed"
)}
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "finished"
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
"ui.panel.config.matter.reinterview_node.interview_complete"
)}
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: nothing}
</ha-dialog>
`;
}
private async _startReinterview(): Promise<void> {
if (!this.hass) {
return;
}
this._status = "started";
try {
await interviewMatterNode(this.hass, this.device_id!);
this._status = "finished";
} catch (err) {
this._status = "failed";
}
}
public closeDialog(): void {
this.device_id = undefined;
this._status = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
.success {
color: var(--success-color);
}
.failed {
color: var(--error-color);
}
.flex-container {
display: flex;
align-items: center;
}
.stages {
margin-top: 16px;
}
.stage ha-svg-icon {
width: 16px;
height: 16px;
}
.stage {
padding: 8px;
}
ha-svg-icon {
width: 68px;
height: 48px;
}
.flex-container ha-circular-progress,
.flex-container ha-svg-icon {
margin-right: 20px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-matter-reinterview-node": DialogMatterReinterviewNode;
}
}

View File

@@ -0,0 +1,19 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
export interface MatterManageFabricsDialogParams {
device_id: string;
}
export const loadManageFabricsDialog = () =>
import("./dialog-matter-manage-fabrics");
export const showMatterManageFabricsDialog = (
element: HTMLElement,
dialogParams: MatterManageFabricsDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-matter-manage-fabrics",
dialogImport: loadManageFabricsDialog,
dialogParams,
});
};

View File

@@ -0,0 +1,19 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
export interface MatterOpenCommissioningWindowDialogParams {
device_id: string;
}
export const loadOpenCommissioningWindowDialog = () =>
import("./dialog-matter-open-commissioning-window");
export const showMatterOpenCommissioningWindowDialog = (
element: HTMLElement,
dialogParams: MatterOpenCommissioningWindowDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-matter-open-commissioning-window",
dialogImport: loadOpenCommissioningWindowDialog,
dialogParams,
});
};

View File

@@ -0,0 +1,18 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
export interface MatterPingNodeDialogParams {
device_id: string;
}
export const loadPingNodeDialog = () => import("./dialog-matter-ping-node");
export const showMatterPingNodeDialog = (
element: HTMLElement,
pingNodeDialogParams: MatterPingNodeDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-matter-ping-node",
dialogImport: loadPingNodeDialog,
dialogParams: pingNodeDialogParams,
});
};

View File

@@ -0,0 +1,19 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
export interface MatterReinterviewNodeDialogParams {
device_id: string;
}
export const loadReinterviewNodeDialog = () =>
import("./dialog-matter-reinterview-node");
export const showMatterReinterviewNodeDialog = (
element: HTMLElement,
reinterviewNodeDialogParams: MatterReinterviewNodeDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-matter-reinterview-node",
dialogImport: loadReinterviewNodeDialog,
dialogParams: reinterviewNodeDialogParams,
});
};

View File

@@ -210,8 +210,8 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
<span slot="secondary">${router.server}</span>
${showOverflow
? html`${network.dataset &&
router.border_agent_id ===
network.dataset.preferred_border_agent_id
router.extended_address ===
network.dataset.preferred_extended_address
? html`<ha-svg-icon
.path=${mdiCellphoneKey}
.title=${this.hass.localize(
@@ -524,12 +524,12 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
dataset: ThreadDataSet,
router: ThreadRouter
) {
const datasetId = dataset.dataset_id;
const borderAgentId = router.border_agent_id;
if (!borderAgentId) {
return;
}
await setPreferredBorderAgent(this.hass, datasetId, borderAgentId);
await setPreferredBorderAgent(
this.hass,
dataset.dataset_id,
router.border_agent_id,
router.extended_address
);
this._refresh();
}

View File

@@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
@@ -56,6 +57,7 @@ class DialogZHAReconfigureDevice extends LitElement {
this._stages = undefined;
this._clusterConfigurationStatuses = undefined;
this._showDetails = false;
this._allSuccessful = true;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -278,7 +280,7 @@ class DialogZHAReconfigureDevice extends LitElement {
(attribute) => html`
<span class="grid-item">
${attribute.name}:
${attribute.success
${attribute.status === "SUCCESS"
? html`
<span class="stage">
<ha-svg-icon
@@ -289,6 +291,12 @@ class DialogZHAReconfigureDevice extends LitElement {
`
: html`
<span class="stage">
<simple-tooltip
animation-delay="0"
position="top"
>
${attribute.status}
</simple-tooltip>
<ha-svg-icon
.path=${mdiCloseCircle}
class="failed"
@@ -361,7 +369,12 @@ class DialogZHAReconfigureDevice extends LitElement {
Object.keys(attributes).forEach((name) => {
const attribute = attributes[name];
clusterConfigurationStatus!.attributes.set(attribute.id, attribute);
this._allSuccessful = this._allSuccessful && attribute.success;
this._allSuccessful =
this._allSuccessful &&
!(
attribute.status in
["FAILURE", "UNSUPPORTED_ATTRIBUTE", "UNREPORTABLE_ATTRIBUTE"]
);
});
}
this.requestUpdate();

View File

@@ -8,11 +8,15 @@ import "../../../components/ha-card";
import "../../../components/ha-list-item";
import "../../../components/ha-svg-icon";
import { domainToName } from "../../../data/integration";
import type { RepairsIssue } from "../../../data/repairs";
import {
fetchRepairsIssueData,
type RepairsIssue,
} from "../../../data/repairs";
import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { showRepairsFlowDialog } from "./show-dialog-repair-flow";
import { showRepairsIssueDialog } from "./show-repair-issue-dialog";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
@customElement("ha-config-repairs")
class HaConfigRepairs extends LitElement {
@@ -107,10 +111,24 @@ class HaConfigRepairs extends LitElement {
`;
}
private _openShowMoreDialog(ev): void {
private async _openShowMoreDialog(ev): Promise<void> {
const issue = ev.currentTarget.issue as RepairsIssue;
if (issue.is_fixable) {
showRepairsFlowDialog(this, issue);
} else if (
issue.domain === "homeassistant" &&
issue.translation_key === "config_entry_reauth"
) {
const data = await fetchRepairsIssueData(
this.hass.connection,
issue.domain,
issue.issue_id
);
if ("flow_id" in data.issue_data) {
showConfigFlowDialog(this, {
continueFlowId: data.issue_data.flow_id as string,
});
}
} else {
showRepairsIssueDialog(this, {
issue,

View File

@@ -191,6 +191,8 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
@blur=${this.handleRippleBlur}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
@@ -204,6 +206,9 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
tabindex=${ifDefined(
hasAction(this._config.tap_action) ? "0" : undefined
)}
style=${styleMap({
"--state-color": colored ? this._computeColor(stateObj) : undefined,
})}
>
${this._config.show_icon
? html`
@@ -217,7 +222,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
.hass=${this.hass}
.stateObj=${stateObj}
style=${styleMap({
color: colored ? this._computeColor(stateObj) : undefined,
filter: colored ? stateColorBrightness(stateObj) : undefined,
height: this._config.icon_height
? this._config.icon_height
@@ -280,23 +284,37 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
this._rippleHandlers.startPress(evt);
}
@eventOptions({ passive: true })
private handleRippleDeactivate() {
this._rippleHandlers.endPress();
}
@eventOptions({ passive: true })
private handleRippleFocus() {
this._rippleHandlers.startFocus();
}
@eventOptions({ passive: true })
private handleRippleBlur() {
this._rippleHandlers.endFocus();
}
@eventOptions({ passive: true })
private handleRippleMouseEnter() {
this._rippleHandlers.startHover();
}
@eventOptions({ passive: true })
private handleRippleMouseLeave() {
this._rippleHandlers.endHover();
}
static get styles(): CSSResultGroup {
return [
iconColorCSS,
css`
ha-card {
--mdc-ripple-color: var(--state-color);
cursor: pointer;
display: flex;
flex-direction: column;
@@ -318,9 +336,11 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
ha-state-icon {
width: 40%;
height: auto;
color: var(--paper-item-icon-color, #44739e);
max-height: 80%;
color: var(--state-color, var(--paper-item-icon-color, #44739e));
--mdc-icon-size: 100%;
--state-inactive-color: var(--paper-item-icon-color, #44739e);
transition: transform 180ms ease-in-out;
}
ha-state-icon + span {
@@ -332,6 +352,11 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
outline: none;
}
:host(:focus-visible) ha-state-icon,
:host(:active) ha-state-icon {
transform: scale(1.2);
}
.state {
font-size: 0.9rem;
color: var(--secondary-text-color);

View File

@@ -238,6 +238,9 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
? this._config.show_names
: true}
.logarithmicScale=${this._config.logarithmic_scale || false}
.minYAxis=${this._config.min_y_axis}
.maxYAxis=${this._config.max_y_axis}
.fitYData=${this._config.fit_y_data || false}
></state-history-charts>
`}
</div>

View File

@@ -166,7 +166,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
.paths=${this._getHistoryPaths(this._config, this._stateHistory)}
.autoFit=${this._config.auto_fit || false}
.fitZones=${this._config.fit_zones}
.darkMode=${this._config.dark_mode}
?darkMode=${this._config.dark_mode}
interactiveZones
renderPassive
></ha-map>

View File

@@ -525,6 +525,9 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
top: -3px;
right: -3px;
}
.icon-container:not([role="button"]) {
pointer-events: none;
}
.icon-container[role="button"]:focus-visible,
.icon-container[role="button"]:active {
transform: scale(1.2);

View File

@@ -324,6 +324,9 @@ export interface HistoryGraphCardConfig extends LovelaceCardConfig {
title?: string;
show_names?: boolean;
logarithmic_scale?: boolean;
min_y_axis?: number;
max_y_axis?: number;
fit_y_data?: boolean;
split_device_classes?: boolean;
}

View File

@@ -1,5 +1,6 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
array,
assert,
@@ -32,29 +33,12 @@ const cardConfigStruct = assign(
refresh_interval: optional(number()), // deprecated
show_names: optional(boolean()),
logarithmic_scale: optional(boolean()),
min_y_axis: optional(number()),
max_y_axis: optional(number()),
fit_y_data: optional(boolean()),
})
);
const SCHEMA = [
{ name: "title", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{
name: "hours_to_show",
default: DEFAULT_HOURS_TO_SHOW,
selector: { number: { min: 1, mode: "box" } },
},
],
},
{
name: "logarithmic_scale",
required: false,
selector: { boolean: {} },
},
] as const;
@customElement("hui-history-graph-card-editor")
export class HuiHistoryGraphCardEditor
extends LitElement
@@ -72,16 +56,69 @@ export class HuiHistoryGraphCardEditor
this._configEntities = processEditorEntities(config.entities);
}
private _schema = memoizeOne(
(showFitOption: boolean) =>
[
{ name: "title", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{
name: "hours_to_show",
default: DEFAULT_HOURS_TO_SHOW,
selector: { number: { min: 1, mode: "box" } },
},
],
},
{
name: "logarithmic_scale",
required: false,
selector: { boolean: {} },
},
{
name: "",
type: "grid",
schema: [
{
name: "min_y_axis",
required: false,
selector: { number: { mode: "box", step: "any" } },
},
{
name: "max_y_axis",
required: false,
selector: { number: { mode: "box", step: "any" } },
},
],
},
...(showFitOption
? [
{
name: "fit_y_data",
required: false,
selector: { boolean: {} },
},
]
: []),
] as const
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const schema = this._schema(
this._config!.min_y_axis !== undefined ||
this._config!.max_y_axis !== undefined
);
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
@@ -106,9 +143,14 @@ export class HuiHistoryGraphCardEditor
fireEvent(this, "config-changed", { config });
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "logarithmic_scale":
case "min_y_axis":
case "max_y_axis":
case "fit_y_data":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.history-graph.${schema.name}`
);

View File

@@ -7,7 +7,6 @@ import {
mdiDotsVertical,
mdiFileMultiple,
mdiFormatListBulletedTriangle,
mdiHelp,
mdiHelpCircle,
mdiMagnify,
mdiPencil,
@@ -75,6 +74,7 @@ import {
} from "../../data/lovelace/config/types";
import { showSaveDialog } from "./editor/show-save-config-dialog";
import { isLegacyStrategyConfig } from "./strategies/legacy-strategy";
import { LocalizeKeys } from "../../common/translations/localize";
@customElement("hui-root")
class HUIRoot extends LitElement {
@@ -110,6 +110,168 @@ class HUIRoot extends LitElement {
);
}
private _renderActionItems(): TemplateResult {
const result: TemplateResult[] = [];
if (this._editMode) {
result.push(
html`<mwc-button
outlined
class="exit-edit-mode"
.label=${this.hass!.localize(
"ui.panel.lovelace.menu.exit_edit_mode"
)}
@click=${this._editModeDisable}
></mwc-button>
<a
href=${documentationUrl(this.hass, "/dashboards/")}
rel="noreferrer"
class="menu-link"
target="_blank"
>
<ha-icon-button
.label=${this.hass!.localize("ui.panel.lovelace.menu.help")}
.path=${mdiHelpCircle}
></ha-icon-button>
</a>`
);
}
const items: {
icon: string;
key: LocalizeKeys;
overflowAction?: any;
buttonAction?: any;
visible: boolean | undefined;
overflow: boolean;
overflow_can_promote?: boolean;
}[] = [
{
icon: mdiFormatListBulletedTriangle,
key: "ui.panel.lovelace.unused_entities.title",
overflowAction: this._handleUnusedEntities,
visible: this._editMode && !__DEMO__,
overflow: true,
},
{
icon: mdiCodeBraces,
key: "ui.panel.lovelace.editor.menu.raw_editor",
overflowAction: this._handleRawEditor,
visible: this._editMode,
overflow: true,
},
{
icon: mdiViewDashboard,
key: "ui.panel.lovelace.editor.menu.manage_dashboards",
overflowAction: this._handleManageDashboards,
visible: this._editMode && !__DEMO__,
overflow: true,
},
{
icon: mdiFileMultiple,
key: "ui.panel.lovelace.editor.menu.manage_resources",
overflowAction: this._handleManageResources,
visible: this._editMode && this.hass.userData?.showAdvanced,
overflow: true,
},
{
icon: mdiMagnify,
key: "ui.panel.lovelace.menu.search",
buttonAction: this._showQuickBar,
overflowAction: this._handleShowQuickBar,
visible: !this._editMode,
overflow: this.narrow,
},
{
icon: mdiCommentProcessingOutline,
key: "ui.panel.lovelace.menu.assist",
buttonAction: this._showVoiceCommandDialog,
overflowAction: this._handleShowVoiceCommandDialog,
visible:
!this._editMode && this._conversation(this.hass.config.components),
overflow: this.narrow,
},
{
icon: mdiRefresh,
key: "ui.common.refresh",
overflowAction: this._handleRefresh,
visible: !this._editMode && this._yamlMode,
overflow: true,
},
{
icon: mdiShape,
key: "ui.panel.lovelace.unused_entities.title",
overflowAction: this._handleUnusedEntities,
visible: !this._editMode && this._yamlMode,
overflow: true,
},
{
icon: mdiRefresh,
key: "ui.panel.lovelace.menu.reload_resources",
overflowAction: this._handleReloadResources,
visible:
!this._editMode &&
(this.hass.panels.lovelace?.config as LovelacePanelConfig)?.mode ===
"yaml",
overflow: true,
},
{
icon: mdiPencil,
key: "ui.panel.lovelace.menu.configure_ui",
overflowAction: this._handleEnableEditMode,
buttonAction: this._enableEditMode,
visible:
!this._editMode &&
this.hass!.user?.is_admin &&
!this.hass!.config.recovery_mode,
overflow: true,
overflow_can_promote: true,
},
];
const overflowItems = items.filter((i) => i.visible && i.overflow);
const overflowCanPromote =
overflowItems.length === 1 && overflowItems[0].overflow_can_promote;
const buttonItems = items.filter(
(i) => i.visible && (!i.overflow || overflowCanPromote)
);
buttonItems.forEach((i) => {
result.push(
html`<ha-icon-button
slot="actionItems"
.label=${this.hass!.localize(i.key)}
.path=${i.icon}
@click=${i.buttonAction}
></ha-icon-button>`
);
});
if (overflowItems.length && !overflowCanPromote) {
const listItems: TemplateResult[] = [];
overflowItems.forEach((i) => {
listItems.push(
html`<mwc-list-item
graphic="icon"
@request-selected=${i.overflowAction}
>
${this.hass!.localize(i.key)}
<ha-svg-icon slot="graphic" .path=${i.icon}></ha-svg-icon>
</mwc-list-item>`
);
});
result.push(
html`<ha-button-menu slot="actionItems">
<ha-icon-button
slot="trigger"
.label=${this.hass!.localize("ui.panel.lovelace.editor.menu.open")}
.path=${mdiDotsVertical}
></ha-icon-button>
${listItems}
</ha-button-menu>`
);
}
return html`${result}`;
}
protected render(): TemplateResult {
const views = this.lovelace?.config.views ?? [];
@@ -139,96 +301,7 @@ class HUIRoot extends LitElement {
@click=${this._editLovelace}
></ha-icon-button>
</div>
<div class="action-items">
<mwc-button
outlined
class="exit-edit-mode"
.label=${this.hass!.localize(
"ui.panel.lovelace.menu.exit_edit_mode"
)}
@click=${this._editModeDisable}
></mwc-button>
<a
href=${documentationUrl(this.hass, "/dashboards/")}
rel="noreferrer"
class="menu-link"
target="_blank"
>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.menu.help"
)}
.path=${mdiHelpCircle}
></ha-icon-button>
</a>
<ha-button-menu>
<ha-icon-button
slot="trigger"
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.menu.open"
)}
.path=${mdiDotsVertical}
></ha-icon-button>
${__DEMO__ /* No unused entities available in the demo */
? ""
: html`
<mwc-list-item
graphic="icon"
@request-selected=${this._handleUnusedEntities}
>
<ha-svg-icon
slot="graphic"
.path=${mdiFormatListBulletedTriangle}
>
</ha-svg-icon>
${this.hass!.localize(
"ui.panel.lovelace.unused_entities.title"
)}
</mwc-list-item>
`}
<mwc-list-item
graphic="icon"
@request-selected=${this._handleRawEditor}
>
<ha-svg-icon
slot="graphic"
.path=${mdiCodeBraces}
></ha-svg-icon>
${this.hass!.localize(
"ui.panel.lovelace.editor.menu.raw_editor"
)}
</mwc-list-item>
${__DEMO__ /* No config available in the demo */
? ""
: html`<mwc-list-item
graphic="icon"
@request-selected=${this._handleManageDashboards}
>
<ha-svg-icon
slot="graphic"
.path=${mdiViewDashboard}
></ha-svg-icon>
${this.hass!.localize(
"ui.panel.lovelace.editor.menu.manage_dashboards"
)}
</mwc-list-item>
${this.hass.userData?.showAdvanced
? html`<mwc-list-item
graphic="icon"
@request-selected=${this
._handleManageResources}
>
<ha-svg-icon
slot="graphic"
.path=${mdiFileMultiple}
></ha-svg-icon>
${this.hass!.localize(
"ui.panel.lovelace.editor.menu.manage_resources"
)}
</mwc-list-item>`
: ""} `}
</ha-button-menu>
</div>
<div class="action-items">${this._renderActionItems()}</div>
`
: html`
${curViewConfig?.subview
@@ -289,174 +362,7 @@ class HUIRoot extends LitElement {
: html`<div class="main-title">
${this.config.title}
</div>`}
<div class="action-items">
${!this.narrow
? html`
<ha-icon-button
slot="actionItems"
.label=${this.hass!.localize(
"ui.panel.lovelace.menu.search"
)}
.path=${mdiMagnify}
@click=${this._showQuickBar}
></ha-icon-button>
`
: ""}
${!this.narrow &&
this._conversation(this.hass.config.components)
? html`
<ha-icon-button
slot="actionItems"
.label=${this.hass!.localize(
"ui.panel.lovelace.menu.assist"
)}
.path=${mdiCommentProcessingOutline}
@click=${this._showVoiceCommandDialog}
></ha-icon-button>
`
: ""}
${this._showButtonMenu
? html`
<ha-button-menu slot="actionItems">
<ha-icon-button
slot="trigger"
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.menu.open"
)}
.path=${mdiDotsVertical}
></ha-icon-button>
${this.narrow
? html`
<mwc-list-item
graphic="icon"
@request-selected=${this
._handleShowQuickBar}
>
${this.hass!.localize(
"ui.panel.lovelace.menu.search"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiMagnify}
></ha-svg-icon>
</mwc-list-item>
`
: ""}
${this.narrow &&
this._conversation(this.hass.config.components)
? html`
<mwc-list-item
graphic="icon"
@request-selected=${this
._handleShowVoiceCommandDialog}
>
${this.hass!.localize(
"ui.panel.lovelace.menu.assist"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiCommentProcessingOutline}
></ha-svg-icon>
</mwc-list-item>
`
: ""}
${this._yamlMode
? html`
<mwc-list-item
graphic="icon"
@request-selected=${this._handleRefresh}
>
${this.hass!.localize("ui.common.refresh")}
<ha-svg-icon
slot="graphic"
.path=${mdiRefresh}
></ha-svg-icon>
</mwc-list-item>
<mwc-list-item
graphic="icon"
@request-selected=${this
._handleUnusedEntities}
>
${this.hass!.localize(
"ui.panel.lovelace.unused_entities.title"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiShape}
></ha-svg-icon>
</mwc-list-item>
`
: ""}
${(
this.hass.panels.lovelace
?.config as LovelacePanelConfig
)?.mode === "yaml"
? html`
<mwc-list-item
graphic="icon"
@request-selected=${this
._handleReloadResources}
>
${this.hass!.localize(
"ui.panel.lovelace.menu.reload_resources"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiRefresh}
></ha-svg-icon>
</mwc-list-item>
`
: ""}
${this.hass!.user?.is_admin &&
!this.hass!.config.recovery_mode
? html`
<mwc-list-item
graphic="icon"
@request-selected=${this
._handleEnableEditMode}
>
${this.hass!.localize(
"ui.panel.lovelace.menu.configure_ui"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiPencil}
></ha-svg-icon>
</mwc-list-item>
`
: ""}
${this._editMode
? html`
<a
href=${documentationUrl(
this.hass,
"/lovelace/"
)}
rel="noreferrer"
class="menu-link"
target="_blank"
>
<mwc-list-item graphic="icon">
${this.hass!.localize(
"ui.panel.lovelace.menu.help"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiHelp}
></ha-svg-icon>
</mwc-list-item>
</a>
`
: ""}
</ha-button-menu>
`
: ""}
</div>
<div class="action-items">${this._renderActionItems()}</div>
`}
</div>
${this._editMode
@@ -698,17 +604,6 @@ class HUIRoot extends LitElement {
return this.shadowRoot!.getElementById("view") as HTMLDivElement;
}
private get _showButtonMenu(): boolean {
return (
(this.narrow && this._conversation(this.hass.config.components)) ||
this._editMode ||
(this.hass!.user?.is_admin && !this.hass!.config.recovery_mode) ||
(this.hass.panels.lovelace?.config as LovelacePanelConfig)?.mode ===
"yaml" ||
this._yamlMode
);
}
private _handleRefresh(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
@@ -811,6 +706,10 @@ class HUIRoot extends LitElement {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._enableEditMode();
}
private _enableEditMode(): void {
if (this._yamlMode) {
showAlertDialog(this, {
text: this.hass!.localize("ui.panel.lovelace.editor.yaml_unsupported"),

View File

@@ -166,6 +166,15 @@ class HaMfaModuleSetupFlow extends LitElement {
ha-markdown a {
color: var(--primary-color);
}
ha-markdown-element p {
text-align: center;
}
ha-markdown-element code {
background-color: transparent;
}
ha-markdown-element > *:last-child {
margin-bottom: revert;
}
.init-spinner {
padding: 10px 100px 34px;
text-align: center;

View File

@@ -73,13 +73,11 @@ export const stateControlCircularSliderStyle = css`
.primary-state {
font-size: 36px;
}
.buttons ha-outlined-icon-button {
--md-outlined-icon-button-container-width: 48px;
--md-outlined-icon-button-container-height: 48px;
--md-outlined-icon-button-icon-size: 24px;
}
.container.md ha-big-number {
font-size: 44px;
}

View File

@@ -1771,6 +1771,7 @@
"update_area": "Update area",
"delete": "Delete",
"name": "Name",
"icon": "Icon",
"name_required": "Name is required",
"area_id": "Area ID",
"unknown_error": "Unknown error",
@@ -2569,7 +2570,8 @@
"to": "To (optional)",
"any_state_ignore_attributes": "Any state (ignoring attribute changes)",
"description": {
"picker": "When the state of an entity (or attribute) changes."
"picker": "When the state of an entity (or attribute) changes.",
"full": "When{hasAttribute, select, \n true { {attribute} of} \n other {}\n} {hasEntity, select, \n true {{entity}} \n other {something}\n} changes{fromChoice, select, \n fromUsed { from {fromString}}\n null { from any state} \n other {}\n}{toChoice, select, \n toUsed { to {toString}} \n null { to any state} \n special { state or any attributes} \n other {}\n}{hasDuration, select, \n true { for {duration}} \n other {}\n}"
}
},
"homeassistant": {
@@ -4596,6 +4598,73 @@
"download_logs": "Download logs"
}
},
"matter": {
"network_type": {
"thread": "Thread",
"wifi": "Wi-Fi",
"ethernet": "Ethernet",
"unknown": "Unknown"
},
"node_type": {
"end_device": "End-device",
"sleepy_end_device": "Sleepy end device",
"routing_end_device": "Routing end device",
"bridge": "Bridge",
"unknown": "Unknown"
},
"device_info": {
"device_info": "Device info",
"node_id": "Node ID",
"network_type": "Network Type",
"node_type": "Device type",
"network_name": "Network name",
"ip_adresses": "IP Address(es)",
"mac_address": "MAC address",
"available": "Available?"
},
"device_actions": {
"reinterview_device": "Re-interview device",
"ping_device": "Ping device",
"open_commissioning_window": "Enable commisisioning mode",
"manage_fabrics": "Manage fabrics",
"view_thread_network": "View Thread network"
},
"manage_fabrics": {
"title": "Connected fabrics",
"fabrics": "Manage the fabrics that have access to this device.",
"remove_fabric_confirm_header": "Remove {fabric} fabric from device",
"remove_fabric_confirm_text": "Are you sure you want to remove the {fabric} from the device? You will not be able to control/access the device from that ecosystem/fabric after this action!",
"remove_fabric_failed_header": "Remove {fabric} fabric failed",
"remove_fabric_failed_text": "The action did not succeed, check the logs for more information."
},
"reinterview_node": {
"title": "Re-interview a Matter device",
"introduction": "Perform a full re-interview of a Matter device. Use this feature only if your device has missing or incorrect functionality.",
"battery_device_warning": "You will need to wake battery powered devices before starting the re-interview. Refer to your device's manual for instructions on how to wake the device.",
"run_in_background": "You can close this dialog and the interview will continue in the background.",
"start_reinterview": "Start re-interview",
"in_progress": "The device is being interviewed. This may take some time.",
"interview_failed": "The device interview failed. Additional information may be available in the logs.",
"interview_complete": "Device interview complete."
},
"ping_node": {
"title": "Ping a Matter device",
"introduction": "Perform a (server-side) ping on your Matter device on all its (known) IP-addresses.",
"battery_device_warning": "Note that especially for battery powered devices this can take a a while. You may need to up powered devices before starting the pinging to speed up the process. Refer to your device's manual for instructions on how to wake the device.",
"start_ping": "Start ping",
"in_progress": "The device is being pinged. This may take some time.",
"ping_failed": "The device ping failed. Additional information may be available in the logs.",
"ping_complete": "Ping device complete."
},
"open_commissioning_window": {
"title": "Enable commissioning mode",
"introduction": "Enable commissioning mode on the device to pair it to another Matter controller.",
"start_commissioning": "Enable commissioning mode",
"in_progress": "We're communicating with the device. This may take some time.",
"failed": "The command failed. Additional information may be available in the logs.",
"sharing_code": "Sharing code"
}
},
"tips": {
"tip": "Tip!",
"join": "Join the community on our {forums}, {twitter}, {discord}, {blog} or {newsletter}",
@@ -5253,7 +5322,10 @@
"history-graph": {
"name": "History graph",
"description": "The History graph card allows you to display a graph for each of the entities listed.",
"logarithmic_scale": "Logarithmic scale"
"logarithmic_scale": "Logarithmic scale",
"min_y_axis": "Y axis minimum",
"max_y_axis": "Y axis maximum",
"fit_y_data": "Extend Y axis limits to fit data"
},
"statistics-graph": {
"name": "Statistics graph",

View File

@@ -14312,6 +14312,13 @@ __metadata:
languageName: node
linkType: hard
"sortablejs@patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch":
version: 1.15.2
resolution: "sortablejs@patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch::version=1.15.2&hash=1591ab"
checksum: d44399e9ca660157c76b13705eaa26191f71c4bd025e2d47b9f7e50a8f9bdb7deaaa2783a8032e55f39627fa4007042bcfd62cb4bbeb2931f6a5d6ee06047e2e
languageName: node
linkType: hard
"source-list-map@npm:^2.0.1":
version: 2.0.1
resolution: "source-list-map@npm:2.0.1"