Compare commits

...

33 Commits

Author SHA1 Message Date
Paul Bottein e642c80003 Fix device automations 2024-10-09 19:28:50 +02:00
Paul Bottein cc07d51613 Add sequence support to dashboard action 2024-10-09 18:53:41 +02:00
karwosts 23a33b10a1 Allow override entity_id in more-info action (#22147) 2024-10-09 14:14:03 +02:00
Simon Lamon 67a93013c7 Revert "Fix drag and drop when using action and trigger selector" (#22296)
Revert "Fix drag and drop when using action and trigger selector (#22291)"

This reverts commit 99035cea8f.
2024-10-09 10:22:40 +00:00
Bram Kragten 1f838d7529 Update statistics issues from dev tools (#22286)
update statistics issues from dev tools
2024-10-09 09:29:30 +02:00
TJ Horner ffc0435144 Fix erroneous addition of service: key in ha-automation-action-play_media element (#22294)
fix: no longer erroneously set 'service' in ha-automation-action-play_media
2024-10-08 21:07:38 +00:00
David F. Mulcahey 5877d69c87 Fix ZHA group dashboard display on mobile (#22279) 2024-10-08 21:20:07 +02:00
Paul Bottein 99035cea8f Fix drag and drop when using action and trigger selector (#22291)
* Fix drag and drop when using action selector

* Fix drag and drop when using trigger selector
2024-10-08 21:04:45 +02:00
__JosephAbbey 1b441a7eec Add support for relative start and end time displays in state-display (#22249)
* Add support for relative start and end time displays in state-display

* Add support for sun attributes as well

* Improve state-display code for relative-time

---------

Co-authored-by: Wendelin <w@pe8.at>
2024-10-08 10:04:16 +02:00
Paul Bottein ad49e9f7b0 Add minimal size for badges and cards in edit mode (#22271) 2024-10-07 17:33:47 +02:00
Petar Petrov e32b15ede2 Hide service dropdown for predefined actions in automations (#22275)
Hide service dropdown for predefined actions
2024-10-07 15:49:44 +02:00
Wendelin a35b4376ea Fix unused entities view (#22274)
Fix compute-unused-entities when using sections
2024-10-07 15:28:14 +02:00
Simon Lamon 619f9f76ee Fixup service/action when entity is picked in activate scene (#22259)
Fixup service/action when entity is picked
2024-10-07 09:23:48 +02:00
dependabot[bot] f771bc10db Bump actions/cache from 4.0.2 to 4.1.0 (#22270)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 08:44:00 +02:00
renovate[bot] b8889a1183 Update dependency eslint-plugin-import to v2.31.0 (#22260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-06 10:47:08 +00:00
renovate[bot] eb6b45eaed Update babel monorepo to v7.25.7 (#22250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 20:11:42 +02:00
renovate[bot] 31a748ed93 Update dependency date-fns-tz to v3.2.0 (#22209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-04 21:38:25 +02:00
Bram Kragten 0110bdd24a Fix and update step flow create (#22223)
* Fix and update step flow create

* cleanup
2024-10-04 14:13:21 +02:00
Bram Kragten 365b712976 Add temporary logging to webrtc player (#22213)
* add temporary logging to webrtc player

* Update ha-web-rtc-player.ts

* Update ha-web-rtc-player.ts

* Update ha-web-rtc-player.ts
2024-10-03 21:01:21 +02:00
Paul Bottein 7d97dbe15b Fix potential undefined select element in color picker (#22212) 2024-10-03 11:31:36 +02:00
Paul Bottein 8bc0ea5a0b Update heading entity schema to allow empty entity id (#22211) 2024-10-03 11:15:13 +02:00
Adam Kapos 44948a3474 Disable backdrop filter on Heading Card (#22204) 2024-10-03 09:45:29 +02:00
Robert Resch bc51b53b4a Use camera ws endpoint to get WebRTC config (#22009) 2024-10-03 09:19:49 +02:00
Bram Kragten 67217b9dd0 Bumped version to 20241002.2 2024-10-02 16:42:46 +02:00
Bram Kragten 487795b7c4 handle unknown state for update voice assitant (#22196)
* handle unknown state for update voice assitant

* Update voice-assistant-setup-step-update.ts
2024-10-02 16:42:27 +02:00
Petar Petrov a30e0d33f9 Handle exceptions when subscribing from the event dev tool (#22191)
* Handle exceptions when subscribing from the event dev tool

* use ha-alert for the error msg

* import ha-alert element

* use undefined instead of null to align with the rest of the code base
2024-10-02 16:34:28 +02:00
Bram Kragten 0c1b8abe03 Fix hassio entrypoint (#22194) 2024-10-02 14:20:53 +00:00
Paul Bottein ce9c5149d5 Use heading card in demo dashboard (#22193) 2024-10-02 14:13:26 +00:00
Bram Kragten adbcdc62eb Alert user when auto update is enabled instead of hiding the button (#22187) 2024-10-02 15:41:15 +02:00
Paul Bottein faf872bfb8 Simplify create automation from device dialog (#22190)
* Simplify automation dialog

* Fix translations

* Auto expand trigger action and condition

* Improve wording

* Expand all

* Remove unused translations
2024-10-02 13:13:21 +00:00
Stefan Agner fe0fb2382a Allow to transfer all Thread datasets with TLV (#22183)
* Allow to transfer all Thread datasets with TLV

This commit allows to transfer all Thread datasets with TLV. Since
PR #22022 the preferred dataset is transmitted when using Matter
external commissioning. This commit makes the Thread configuration
dialog to have feature parity.

* Drop preferred border agent id as additional metric for default router

We always have the extended address, so use this as primary and only
metric which router is the default. The preferred border agent id gets
updated best effort.

Also use isDefaultRouter consistently in the code.
2024-10-02 15:06:06 +02:00
Bram Kragten cdd29295e5 Bumped version to 20241002.1 2024-10-02 13:37:47 +02:00
karwosts f7532f3476 Devtools statistics - new style, multi-select, & multi-delete (#21813)
* feat: auto-fix statistics

* statistics multi-select and multi-fix

* unused css

* Change multi action to clear, fixes

* Update developer-tools-statistics.ts

* update translations

* Add select all issues option

* Update en.json

* Update developer-tools-statistics.ts

---------

Co-authored-by: Muka Schultze <samuelschultze@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-10-02 13:21:04 +02:00
54 changed files with 2163 additions and 1296 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@v4.0.2
uses: actions/cache@v4.1.0
with:
path: |
node_modules/.cache/prettier
+26
View File
@@ -111,6 +111,16 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
friendly_name: "Living room Temperature",
},
},
"sensor.living_room_humidity": {
entity_id: "sensor.living_room_humidity",
state: "57",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "humidity",
friendly_name: "Living room Humidity",
},
},
"sensor.outdoor_temperature": {
entity_id: "sensor.outdoor_temperature",
state: "10.5",
@@ -189,6 +199,14 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
supported_features: 32,
},
},
"binary_sensor.kitchen_motion": {
entity_id: "light.kitchen_motion",
state: "on",
attributes: {
device_class: "motion",
friendly_name: "Kitchen motion",
},
},
"light.worktop_spotlights": {
entity_id: "light.worktop_spotlights",
state: "off",
@@ -423,6 +441,14 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
supported_features: 64063,
},
},
"switch.in_meeting": {
entity_id: "switch.in_meeting",
state: "on",
attributes: {
icon: "mdi:laptop-account",
friendly_name: "In a meeting",
},
},
"sensor.standing_desk_height": {
entity_id: "sensor.standing_desk_height",
state: "72",
+97 -16
View File
@@ -30,12 +30,36 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
? []
: [
{
title: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
cards: [{ type: "custom:ha-demo-card" }],
cards: [
{
type: "heading",
heading: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
},
{ type: "custom:ha-demo-card" },
],
},
]),
{
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.living_room"
),
icon: "mdi:sofa",
badges: [
{
type: "entity",
entity: "sensor.living_room_temperature",
color: "red",
},
{
type: "entity",
entity: "sensor.living_room_humidity",
color: "indigo",
},
],
},
{
type: "tile",
entity: "light.floor_lamp",
@@ -54,13 +78,6 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
type: "tile",
entity: "light.bar_lamp",
},
{
graph: "line",
type: "sensor",
entity: "sensor.living_room_temperature",
detail: 1,
name: "Temperature",
},
{
type: "tile",
entity: "cover.living_room_garden_shutter",
@@ -71,11 +88,25 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
entity: "media_player.living_room_nest_mini",
},
],
title: `🛋️ ${localize("ui.panel.page-demo.config.sections.titles.living_room")} `,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.kitchen"
),
icon: "mdi:fridge",
badges: [
{
type: "entity",
entity: "binary_sensor.kitchen_motion",
show_state: false,
color: "blue",
},
],
},
{
type: "tile",
entity: "cover.kitchen_shutter",
@@ -106,11 +137,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
entity: "media_player.kitchen_nest_audio",
},
],
title: `👩‍🍳 ${localize("ui.panel.page-demo.config.sections.titles.kitchen")}`,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.energy"
),
icon: "mdi:transmission-tower",
},
{
type: "tile",
entity: "binary_sensor.tesla_wall_connector_vehicle_connected",
@@ -148,11 +185,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
color: "dark-grey",
},
],
title: `⚡️ ${localize("ui.panel.page-demo.config.sections.titles.energy")}`,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.climate"
),
icon: "mdi:thermometer",
},
{
type: "tile",
entity: "sun.sun",
@@ -185,16 +228,38 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
state_content: ["preset_mode", "current_temperature"],
},
],
title: `🌤️ ${localize("ui.panel.page-demo.config.sections.titles.climate")}`,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.study"
),
icon: "mdi:desk-lamp",
badges: [
{
type: "entity",
entity: "switch.in_meeting",
state: "on",
state_content: "name",
visibility: [
{
condition: "state",
state: "on",
entity: "switch.in_meeting",
},
],
},
],
},
{
type: "tile",
entity: "cover.study_shutter",
name: "Shutter",
},
{
type: "tile",
entity: "light.study_spotlights",
@@ -211,12 +276,23 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
color: "brown",
icon: "mdi:desk",
},
{
type: "tile",
entity: "switch.in_meeting",
name: "Meeting mode",
},
],
title: `🧑‍💻 ${localize("ui.panel.page-demo.config.sections.titles.study")}`,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.outdoor"
),
icon: "mdi:tree",
},
{
type: "tile",
entity: "light.outdoor_light",
@@ -246,11 +322,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
name: "Illuminance",
},
],
title: `🌳 ${localize("ui.panel.page-demo.config.sections.titles.outdoor")}`,
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.updates"
),
icon: "mdi:update",
},
{
type: "tile",
entity: "automation.home_assistant_auto_update",
@@ -276,7 +358,6 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
icon: "mdi:home-assistant",
},
],
title: `🎉 ${localize("ui.panel.page-demo.config.sections.titles.updates")}`,
},
],
},
+2 -1
View File
@@ -13,10 +13,11 @@
<% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>");
<% } %>
}
} else {
<% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>");
<% } %>
}
}
})();
+8 -8
View File
@@ -25,7 +25,7 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.25.6",
"@babel/runtime": "7.25.7",
"@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.1",
"@codemirror/commands": "6.6.2",
@@ -104,7 +104,7 @@
"core-js": "3.38.1",
"cropperjs": "1.6.2",
"date-fns": "4.1.0",
"date-fns-tz": "3.1.3",
"date-fns-tz": "3.2.0",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
@@ -151,12 +151,12 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.25.2",
"@babel/core": "7.25.7",
"@babel/helper-define-polyfill-provider": "0.6.2",
"@babel/plugin-proposal-decorators": "7.24.7",
"@babel/plugin-transform-runtime": "7.25.4",
"@babel/preset-env": "7.25.4",
"@babel/preset-typescript": "7.24.7",
"@babel/plugin-proposal-decorators": "7.25.7",
"@babel/plugin-transform-runtime": "7.25.7",
"@babel/preset-env": "7.25.7",
"@babel/preset-typescript": "7.25.7",
"@bundle-stats/plugin-webpack-filter": "4.15.1",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.7.0",
@@ -201,7 +201,7 @@
"eslint-config-airbnb-typescript": "18.0.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-webpack": "0.13.9",
"eslint-plugin-import": "2.30.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-lit": "1.15.0",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.4",
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20241002.0"
version = "20241002.2"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
@@ -204,6 +204,29 @@ export class HaDataTable extends LitElement {
this._checkedRowsChanged();
}
public select(ids: string[], clear?: boolean): void {
if (clear) {
this._checkedRows = [];
}
ids.forEach((id) => {
const row = this._filteredData.find((data) => data[this.id] === id);
if (row?.selectable !== false && !this._checkedRows.includes(id)) {
this._checkedRows.push(id);
}
});
this._checkedRowsChanged();
}
public unselect(ids: string[]): void {
ids.forEach((id) => {
const index = this._checkedRows.indexOf(id);
if (index > -1) {
this._checkedRows.splice(index, 1);
}
});
this._checkedRowsChanged();
}
public connectedCallback() {
super.connectedCallback();
if (this._filteredData.length) {
@@ -3,6 +3,7 @@ import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { fullEntitiesContext } from "../../data/context";
import {
DeviceAutomation,
@@ -103,6 +104,7 @@ export abstract class HaDeviceAutomationPicker<
.label=${this.label}
.value=${value}
@selected=${this._automationChanged}
@closed=${stopPropagation}
.disabled=${this._automations.length === 0}
>
${value === NO_AUTOMATION_KEY
+2 -2
View File
@@ -33,12 +33,12 @@ export class HaColorPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-select") private _select!: HaSelect;
@query("ha-select") private _select?: HaSelect;
connectedCallback(): void {
super.connectedCallback();
// Refresh layout options when the field is connected to the DOM to ensure current value displayed
this._select.layoutOptions();
this._select?.layoutOptions();
}
private _valueSelected(ev) {
+1
View File
@@ -174,6 +174,7 @@ export class HaServiceControl extends LitElement {
if (this._value && serviceData) {
const loadDefaults = this.value && !("data" in this.value);
// Set mandatory bools without a default value to false
this._value = { ...this._value };
if (!this._value.data) {
this._value.data = {};
}
+53 -34
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import {
css,
CSSResultGroup,
@@ -6,11 +7,14 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { handleWebRtcOffer, WebRtcAnswer } from "../data/camera";
import { fetchWebRtcSettings } from "../data/rtsp_to_webrtc";
import {
fetchWebRtcClientConfiguration,
handleWebRtcOffer,
WebRtcAnswer,
} from "../data/camera";
import type { HomeAssistant } from "../types";
import "./ha-alert";
@@ -37,12 +41,11 @@ class HaWebRtcPlayer extends LitElement {
@property({ type: Boolean, attribute: "playsinline" })
public playsInline = false;
@property() public posterUrl!: string;
@property({ attribute: "poster-url" }) public posterUrl?: string;
@state() private _error?: string;
// don't cache this, as we remove it on disconnects
@query("#remote-stream") private _videoEl!: HTMLVideoElement;
@query("#remote-stream", true) private _videoEl!: HTMLVideoElement;
private _peerConnection?: RTCPeerConnection;
@@ -59,7 +62,7 @@ class HaWebRtcPlayer extends LitElement {
.muted=${this.muted}
?playsinline=${this.playsInline}
?controls=${this.controls}
.poster=${this.posterUrl}
poster=${ifDefined(this.posterUrl)}
@loadeddata=${this._loadedData}
></video>
`;
@@ -81,20 +84,30 @@ class HaWebRtcPlayer extends LitElement {
if (!changedProperties.has("entityid")) {
return;
}
if (!this._videoEl) {
return;
}
this._startWebRtc();
}
private async _startWebRtc(): Promise<void> {
console.time("WebRTC");
this._error = undefined;
const configuration = await this._fetchPeerConfiguration();
const peerConnection = new RTCPeerConnection(configuration);
// Some cameras (such as nest) require a data channel to establish a stream
// however, not used by any integrations.
peerConnection.createDataChannel("dataSendChannel");
console.timeLog("WebRTC", "start clientConfig");
const clientConfig = await fetchWebRtcClientConfiguration(
this.hass,
this.entityid
);
console.timeLog("WebRTC", "end clientConfig", clientConfig);
const peerConnection = new RTCPeerConnection(clientConfig.configuration);
if (clientConfig.dataChannel) {
// Some cameras (such as nest) require a data channel to establish a stream
// however, not used by any integrations.
peerConnection.createDataChannel(clientConfig.dataChannel);
}
peerConnection.addTransceiver("audio", { direction: "recvonly" });
peerConnection.addTransceiver("video", { direction: "recvonly" });
@@ -102,30 +115,48 @@ class HaWebRtcPlayer extends LitElement {
offerToReceiveAudio: true,
offerToReceiveVideo: true,
};
console.timeLog("WebRTC", "start createOffer", offerOptions);
const offer: RTCSessionDescriptionInit =
await peerConnection.createOffer(offerOptions);
console.timeLog("WebRTC", "end createOffer", offer);
console.timeLog("WebRTC", "start setLocalDescription");
await peerConnection.setLocalDescription(offer);
console.timeLog("WebRTC", "end setLocalDescription");
console.timeLog("WebRTC", "start iceResolver");
let candidates = ""; // Build an Offer SDP string with ice candidates
const iceResolver = new Promise<void>((resolve) => {
peerConnection.addEventListener("icecandidate", async (event) => {
peerConnection.addEventListener("icecandidate", (event) => {
if (!event.candidate?.candidate) {
resolve(); // Gathering complete
return;
}
console.timeLog("WebRTC", "iceResolver candidate", event.candidate);
candidates += `a=${event.candidate.candidate}\r\n`;
});
});
await iceResolver;
console.timeLog("WebRTC", "end iceResolver", candidates);
const offer_sdp = offer.sdp! + candidates;
let webRtcAnswer: WebRtcAnswer;
try {
console.timeLog("WebRTC", "start WebRTCOffer", offer_sdp);
webRtcAnswer = await handleWebRtcOffer(
this.hass,
this.entityid,
offer_sdp
);
console.timeLog("WebRTC", "end webRtcOffer", webRtcAnswer);
} catch (err: any) {
this._error = "Failed to start WebRTC stream: " + err.message;
peerConnection.close();
@@ -135,6 +166,7 @@ class HaWebRtcPlayer extends LitElement {
// Setup callbacks to render remote stream once media tracks are discovered.
const remoteStream = new MediaStream();
peerConnection.addEventListener("track", (event) => {
console.timeLog("WebRTC", "track", event);
remoteStream.addTrack(event.track);
this._videoEl.srcObject = remoteStream;
});
@@ -146,7 +178,9 @@ class HaWebRtcPlayer extends LitElement {
sdp: webRtcAnswer.answer,
});
try {
console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc);
await peerConnection.setRemoteDescription(remoteDesc);
console.timeLog("WebRTC", "end setRemoteDescription");
} catch (err: any) {
this._error = "Failed to connect WebRTC stream: " + err.message;
peerConnection.close();
@@ -155,23 +189,6 @@ class HaWebRtcPlayer extends LitElement {
this._peerConnection = peerConnection;
}
private async _fetchPeerConfiguration(): Promise<RTCConfiguration> {
if (!isComponentLoaded(this.hass!, "rtsp_to_webrtc")) {
return {};
}
const settings = await fetchWebRtcSettings(this.hass!);
if (!settings || !settings.stun_server) {
return {};
}
return {
iceServers: [
{
urls: [`stun:${settings.stun_server!}`],
},
],
};
}
private _cleanUp() {
if (this._remoteStream) {
this._remoteStream.getTracks().forEach((track) => {
@@ -190,6 +207,8 @@ class HaWebRtcPlayer extends LitElement {
}
private _loadedData() {
console.timeLog("WebRTC", "loadedData");
console.timeEnd("WebRTC");
// @ts-ignore
fireEvent(this, "load");
}
+7 -2
View File
@@ -8,6 +8,7 @@ import { Context, HomeAssistant } from "../types";
import { BlueprintInput } from "./blueprint";
import { DeviceCondition, DeviceTrigger } from "./device_automation";
import { Action, MODES, migrateAutomationAction } from "./script";
import { createSearchParam } from "../common/url/search-params";
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
export const AUTOMATION_DEFAULT_MAX = 10;
@@ -462,9 +463,13 @@ export const flattenTriggers = (
return flatTriggers;
};
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
export const showAutomationEditor = (
data?: Partial<AutomationConfig>,
expanded?: boolean
) => {
initialAutomationEditorData = data;
navigate("/config/automation/edit/new");
const params = expanded ? `?${createSearchParam({ expanded: "1" })}` : "";
navigate(`/config/automation/edit/new${params}`);
};
export const duplicateAutomation = (config: AutomationConfig) => {
+14
View File
@@ -133,3 +133,17 @@ export const isCameraMediaSource = (mediaContentId: string) =>
export const getEntityIdFromCameraMediaSource = (mediaContentId: string) =>
mediaContentId.substring(CAMERA_MEDIA_SOURCE_PREFIX.length);
export interface WebRTCClientConfiguration {
configuration: RTCConfiguration;
dataChannel?: string;
}
export const fetchWebRtcClientConfiguration = async (
hass: HomeAssistant,
entityId: string
) =>
hass.callWS<WebRTCClientConfiguration>({
type: "camera/webrtc/get_client_config",
entity_id: entityId,
});
+9 -1
View File
@@ -1,4 +1,5 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { Action } from "../../script";
export interface ToggleActionConfig extends BaseActionConfig {
action: "toggle";
@@ -28,6 +29,12 @@ export interface UrlActionConfig extends BaseActionConfig {
export interface MoreInfoActionConfig extends BaseActionConfig {
action: "more-info";
entity_id?: string;
}
export interface SequenceActionConfig extends BaseActionConfig {
action: "sequence";
actions?: Action[];
}
export interface AssistActionConfig extends BaseActionConfig {
@@ -66,4 +73,5 @@ export type ActionConfig =
| MoreInfoActionConfig
| AssistActionConfig
| NoActionConfig
| CustomActionConfig;
| CustomActionConfig
| SequenceActionConfig;
-10
View File
@@ -1,10 +0,0 @@
import { HomeAssistant } from "../types";
export interface WebRtcSettings {
stun_server?: string;
}
export const fetchWebRtcSettings = async (hass: HomeAssistant) =>
hass.callWS<WebRtcSettings>({
type: "rtsp_to_webrtc/get_settings",
});
+7 -2
View File
@@ -28,6 +28,7 @@ import {
} from "./automation";
import { BlueprintInput } from "./blueprint";
import { computeObjectId } from "../common/entity/compute_object_id";
import { createSearchParam } from "../common/url/search-params";
export const MODES = ["single", "restart", "queued", "parallel"] as const;
export const MODES_MAX = ["queued", "parallel"] as const;
@@ -347,9 +348,13 @@ export const getScriptStateConfig = (hass: HomeAssistant, entity_id: string) =>
entity_id,
});
export const showScriptEditor = (data?: Partial<ScriptConfig>) => {
export const showScriptEditor = (
data?: Partial<ScriptConfig>,
expanded?: boolean
) => {
inititialScriptEditorData = data;
navigate("/config/script/edit/new");
const params = expanded ? `?${createSearchParam({ expanded: "1" })}` : "";
navigate(`/config/script/edit/new${params}`);
};
export const getScriptEditorInitData = () => {
+1 -1
View File
@@ -18,7 +18,7 @@ export interface ThreadDataSet {
channel: number | null;
created: string;
dataset_id: string;
extended_pan_id: string | null;
extended_pan_id: string;
network_name: string;
pan_id: string | null;
preferred_border_agent_id: string | null;
@@ -9,23 +9,15 @@ import {
html,
nothing,
} from "lit";
import { customElement, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { HASSDomEvent, fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-circular-progress";
import "../../components/ha-dialog";
import "../../components/ha-icon-button";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import {
DataEntryFlowStep,
subscribeDataEntryFlowProgressed,
} from "../../data/data_entry_flow";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
@@ -62,7 +54,7 @@ declare global {
@customElement("dialog-data-entry-flow")
class DataEntryFlowDialog extends LitElement {
public hass!: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: DataEntryFlowDialogParams;
@@ -76,16 +68,8 @@ class DataEntryFlowDialog extends LitElement {
// Null means we need to pick a config flow
| null;
@state() private _devices?: DeviceRegistryEntry[];
@state() private _areas?: AreaRegistryEntry[];
@state() private _handler?: string;
private _unsubAreas?: UnsubscribeFunc;
private _unsubDevices?: UnsubscribeFunc;
private _unsubDataEntryFlowProgressed?: Promise<UnsubscribeFunc>;
public async showDialog(params: DataEntryFlowDialogParams): Promise<void> {
@@ -183,16 +167,7 @@ class DataEntryFlowDialog extends LitElement {
this._loading = undefined;
this._step = undefined;
this._params = undefined;
this._devices = undefined;
this._handler = undefined;
if (this._unsubAreas) {
this._unsubAreas();
this._unsubAreas = undefined;
}
if (this._unsubDevices) {
this._unsubDevices();
this._unsubDevices = undefined;
}
if (this._unsubDataEntryFlowProgressed) {
this._unsubDataEntryFlowProgressed.then((unsub) => {
unsub();
@@ -309,25 +284,13 @@ class DataEntryFlowDialog extends LitElement {
.hass=${this.hass}
></step-flow-menu>
`
: this._devices === undefined ||
this._areas === undefined
? // When it's a create entry result, we will fetch device & area registry
html`
<step-flow-loading
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
loadingReason="loading_devices_areas"
></step-flow-loading>
`
: html`
<step-flow-create-entry
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.devices=${this._devices}
.areas=${this._areas}
></step-flow-create-entry>
`}
: html`
<step-flow-create-entry
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-create-entry>
`}
`}
</div>
</ha-dialog>
@@ -351,32 +314,6 @@ class DataEntryFlowDialog extends LitElement {
// external and progress step will send update event from the backend, so we should subscribe to them
this._subscribeDataEntryFlowProgressed();
}
if (this._step.type === "create_entry") {
if (this._step.result && this._params!.flowConfig.loadDevicesAndAreas) {
this._fetchDevices(this._step.result.entry_id);
this._fetchAreas();
} else {
this._devices = [];
this._areas = [];
}
}
}
private async _fetchDevices(configEntryId) {
this._unsubDevices = subscribeDeviceRegistry(
this.hass.connection,
(devices) => {
this._devices = devices.filter((device) =>
device.config_entries.includes(configEntryId)
);
}
);
}
private async _fetchAreas() {
this._unsubAreas = subscribeAreaRegistry(this.hass.connection, (areas) => {
this._areas = areas;
});
}
private async _processStep(
@@ -20,7 +20,7 @@ export const showConfigFlowDialog = (
): void =>
showFlowDialog(element, dialogParams, {
flowType: "config_flow",
loadDevicesAndAreas: true,
showDevices: true,
createFlow: async (hass, handler) => {
const [step] = await Promise.all([
createConfigFlow(hass, handler, dialogParams.entryId),
@@ -17,7 +17,7 @@ import type { HomeAssistant } from "../../types";
export interface FlowConfig {
flowType: FlowType;
loadDevicesAndAreas: boolean;
showDevices: boolean;
createFlow(hass: HomeAssistant, handler: string): Promise<DataEntryFlowStep>;
@@ -134,8 +134,7 @@ export interface FlowConfig {
export type LoadingReason =
| "loading_handlers"
| "loading_flow"
| "loading_step"
| "loading_devices_areas";
| "loading_step";
export interface DataEntryFlowDialogParams {
startFlowHandler?: string;
@@ -29,7 +29,7 @@ export const showOptionsFlowDialog = (
},
{
flowType: "options_flow",
loadDevicesAndAreas: false,
showDevices: false,
createFlow: async (hass, handler) => {
const [step] = await Promise.all([
createOptionsFlow(hass, handler),
@@ -4,6 +4,7 @@ import {
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
TemplateResult,
} from "lit";
@@ -34,7 +35,16 @@ class StepFlowCreateEntry extends LitElement {
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
@property({ attribute: false }) public devices!: DeviceRegistryEntry[];
private _devices = memoizeOne(
(
showDevices: boolean,
devices: DeviceRegistryEntry[],
entry_id?: string
) =>
showDevices && entry_id
? devices.filter((device) => device.config_entries.includes(entry_id))
: []
);
private _deviceEntities = memoizeOne(
(
@@ -50,35 +60,48 @@ class StepFlowCreateEntry extends LitElement {
);
protected willUpdate(changedProps: PropertyValues) {
if (!changedProps.has("devices") && !changedProps.has("hass")) {
return;
}
const devices = this._devices(
this.flowConfig.showDevices,
Object.values(this.hass.devices),
this.step.result?.entry_id
);
if (
(changedProps.has("devices") || changedProps.has("hass")) &&
this.devices.length === 1
devices.length !== 1 ||
devices[0].primary_config_entry !== this.step.result?.entry_id
) {
// integration_type === "device"
const assistSatellites = this._deviceEntities(
this.devices[0].id,
Object.values(this.hass.entities),
"assist_satellite"
);
if (
assistSatellites.length &&
assistSatellites.some((satellite) =>
assistSatelliteSupportsSetupFlow(
this.hass.states[satellite.entity_id]
)
)
) {
this._flowDone();
showVoiceAssistantSetupDialog(this, {
deviceId: this.devices[0].id,
});
}
return;
}
const assistSatellites = this._deviceEntities(
devices[0].id,
Object.values(this.hass.entities),
"assist_satellite"
);
if (
assistSatellites.length &&
assistSatellites.some((satellite) =>
assistSatelliteSupportsSetupFlow(this.hass.states[satellite.entity_id])
)
) {
this._flowDone();
showVoiceAssistantSetupDialog(this, {
deviceId: devices[0].id,
});
}
}
protected render(): TemplateResult {
const localize = this.hass.localize;
const devices = this._devices(
this.flowConfig.showDevices,
Object.values(this.hass.devices),
this.step.result?.entry_id
);
return html`
<h2>${localize("ui.panel.config.integrations.config_flow.success")}!</h2>
<div class="content">
@@ -89,9 +112,9 @@ class StepFlowCreateEntry extends LitElement {
"ui.panel.config.integrations.config_flow.not_loaded"
)}</span
>`
: ""}
${this.devices.length === 0
? ""
: nothing}
${devices.length === 0
? nothing
: html`
<p>
${localize(
@@ -99,7 +122,7 @@ class StepFlowCreateEntry extends LitElement {
)}:
</p>
<div class="devices">
${this.devices.map(
${devices.map(
(device) => html`
<div class="device">
<div>
@@ -18,6 +18,7 @@ import {
updateReleaseNotes,
} from "../../../data/update";
import type { HomeAssistant } from "../../../types";
import { showAlertDialog } from "../../generic/show-dialog-box";
@customElement("more-info-update")
class MoreInfoUpdate extends LitElement {
@@ -127,29 +128,27 @@ class MoreInfoUpdate extends LitElement {
</ha-formfield> `
: ""}
<div class="actions">
${this.stateObj.attributes.auto_update
? ""
: this.stateObj.state === BINARY_STATE_OFF &&
this.stateObj.attributes.skipped_version
? html`
<mwc-button @click=${this._handleClearSkipped}>
${this.hass.localize(
"ui.dialogs.more_info_control.update.clear_skipped"
)}
</mwc-button>
`
: html`
<mwc-button
@click=${this._handleSkip}
.disabled=${skippedVersion ||
this.stateObj.state === BINARY_STATE_OFF ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.skip"
)}
</mwc-button>
`}
${this.stateObj.state === BINARY_STATE_OFF &&
this.stateObj.attributes.skipped_version
? html`
<mwc-button @click=${this._handleClearSkipped}>
${this.hass.localize(
"ui.dialogs.more_info_control.update.clear_skipped"
)}
</mwc-button>
`
: html`
<mwc-button
@click=${this._handleSkip}
.disabled=${skippedVersion ||
this.stateObj.state === BINARY_STATE_OFF ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.skip"
)}
</mwc-button>
`}
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
? html`
<mwc-button
@@ -211,6 +210,17 @@ class MoreInfoUpdate extends LitElement {
}
private _handleSkip(): void {
if (this.stateObj!.attributes.auto_update) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.dialogs.more_info_control.update.auto_update_enabled_title"
),
text: this.hass.localize(
"ui.dialogs.more_info_control.update.auto_update_enabled_text"
),
});
return;
}
this.hass.callService("update", "skip", {
entity_id: this.stateObj!.entity_id,
});
@@ -2,7 +2,7 @@ import { css, html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-circular-progress";
import { OFF, ON, UNAVAILABLE } from "../../data/entity";
import { OFF, ON, UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { HomeAssistant } from "../../types";
import { AssistantSetupStyles } from "./styles";
@@ -32,10 +32,11 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
if (
(oldState?.state === UNAVAILABLE &&
newState?.state !== UNAVAILABLE) ||
(oldState?.state === OFF && newState?.state === ON)
(oldState?.state !== ON && newState?.state === ON)
) {
// Device is rebooted, let's move on
this._tryUpdate(false);
return;
}
}
}
@@ -58,7 +59,7 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
return html`<div class="content">
<img src="/static/icons/casita/loading.png" />
<h1>
${stateObj.state === OFF
${stateObj.state === OFF || stateObj.state === UNKNOWN
? "Checking for updates"
: "Updating your voice assistant"}
</h1>
@@ -88,10 +89,7 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
return;
}
const updateEntity = this.hass.states[this.updateEntityId];
if (
updateEntity &&
this.hass.states[updateEntity.entity_id].state === "on"
) {
if (updateEntity && this.hass.states[updateEntity.entity_id].state === ON) {
this._updated = true;
await this.hass.callService(
"update",
+3 -2
View File
@@ -141,9 +141,10 @@ interface EMOutgoingMessageImprovScan extends EMMessage {
interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
type: "thread/store_in_platform_keychain";
payload: {
mac_extended_address: string;
border_agent_id: string;
mac_extended_address: string | null;
border_agent_id: string | null;
active_operational_dataset: string;
extended_pan_id: string;
};
}
@@ -156,6 +156,15 @@ export default class HaAutomationAction extends LitElement {
}
}
public expandAll() {
const rows = this.shadowRoot!.querySelectorAll<HaAutomationActionRow>(
"ha-automation-action-row"
)!;
rows.forEach((row) => {
row.expand();
});
}
private _addActionDialog() {
showAddAutomationElementDialog(this, {
type: "action",
@@ -55,12 +55,12 @@ export class HaSceneAction extends LitElement implements ActionElement {
fireEvent(this, "value-changed", {
value: {
...this.action,
service: "scene.turn_on",
action: "scene.turn_on",
target: {
entity_id: ev.detail.value,
},
metadata: {},
},
} as SceneAction,
});
}
}
@@ -52,7 +52,7 @@ export class HaPlayMediaAction extends LitElement implements ActionElement {
fireEvent(this, "value-changed", {
value: {
...this.action,
service: "media_player.play_media",
action: "media_player.play_media",
target: { entity_id: ev.detail.value.entity_id },
data: {
media_content_id: ev.detail.value.media_content_id,
@@ -117,6 +117,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
.value=${this._action}
.disabled=${this.disabled}
.showAdvanced=${this.hass.userData?.showAdvanced}
.hidePicker=${!!this._action.metadata}
@value-changed=${this._actionChanged}
></ha-service-control>
${domain && service && this.hass.services[domain]?.[service]?.response
@@ -106,6 +106,15 @@ export default class HaAutomationCondition extends LitElement {
}
}
public expandAll() {
const rows = this.shadowRoot!.querySelectorAll<HaAutomationConditionRow>(
"ha-automation-condition-row"
)!;
rows.forEach((row) => {
row.expand();
});
}
private get nested() {
return this.path !== undefined;
}
@@ -1,7 +1,14 @@
import "@material/mwc-button/mwc-button";
import { mdiHelpCircle } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property } from "lit/decorators";
import { ensureArray } from "../../../common/array/ensure-array";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -21,6 +28,14 @@ import { documentationUrl } from "../../../util/documentation-url";
import "./action/ha-automation-action";
import "./condition/ha-automation-condition";
import "./trigger/ha-automation-trigger";
import type HaAutomationTrigger from "./trigger/ha-automation-trigger";
import type HaAutomationAction from "./action/ha-automation-action";
import type HaAutomationCondition from "./condition/ha-automation-condition";
import {
extractSearchParam,
removeSearchParam,
} from "../../../common/url/search-params";
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
@customElement("manual-automation-editor")
export class HaManualAutomationEditor extends LitElement {
@@ -36,6 +51,31 @@ export class HaManualAutomationEditor extends LitElement {
@property({ attribute: false }) public stateObj?: HassEntity;
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
const expanded = extractSearchParam("expanded");
if (expanded === "1") {
this._clearParam("expanded");
const items = this.shadowRoot!.querySelectorAll<
HaAutomationTrigger | HaAutomationCondition | HaAutomationAction
>("ha-automation-trigger, ha-automation-condition, ha-automation-action");
items.forEach((el) => {
el.updateComplete.then(() => {
el.expandAll();
});
});
}
}
private _clearParam(param: string) {
window.history.replaceState(
null,
"",
constructUrlCurrentPath(removeSearchParam(param))
);
}
protected render() {
return html`
${this.stateObj?.state === "off"
@@ -179,6 +179,15 @@ export default class HaAutomationTrigger extends LitElement {
}
}
public expandAll() {
const rows = this.shadowRoot!.querySelectorAll<HaAutomationTriggerRow>(
"ha-automation-trigger-row"
)!;
rows.forEach((row) => {
row.expand();
});
}
private _getKey(action: Trigger) {
if (!this._triggerKeys.has(action)) {
this._triggerKeys.set(action, Math.random().toString());
@@ -1,23 +0,0 @@
import { customElement } from "lit/decorators";
import {
DeviceAction,
localizeDeviceAutomationAction,
} from "../../../../data/device_automation";
import { HaDeviceAutomationCard } from "./ha-device-automation-card";
@customElement("ha-device-actions-card")
export class HaDeviceActionsCard extends HaDeviceAutomationCard<DeviceAction> {
readonly type = "action";
readonly headerKey = "ui.panel.config.devices.automation.actions.caption";
constructor() {
super(localizeDeviceAutomationAction);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-device-actions-card": HaDeviceActionsCard;
}
}
@@ -1,142 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/chips/ha-assist-chip";
import "../../../../components/chips/ha-chip-set";
import { showAutomationEditor } from "../../../../data/automation";
import {
DeviceAction,
DeviceAutomation,
} from "../../../../data/device_automation";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { showScriptEditor } from "../../../../data/script";
import { buttonLinkStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
declare global {
interface HASSDomEvents {
"entry-selected": undefined;
}
}
export abstract class HaDeviceAutomationCard<
T extends DeviceAutomation,
> extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public deviceId?: string;
@property({ type: Boolean }) public script = false;
@property({ attribute: false }) public automations: T[] = [];
@property({ attribute: false }) entityReg?: EntityRegistryEntry[];
@state() public _showSecondary = false;
abstract headerKey: Parameters<typeof this.hass.localize>[0];
abstract type: "action" | "condition" | "trigger";
private _localizeDeviceAutomation: (
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
automation: T
) => string;
constructor(
localizeDeviceAutomation: HaDeviceAutomationCard<T>["_localizeDeviceAutomation"]
) {
super();
this._localizeDeviceAutomation = localizeDeviceAutomation;
}
protected shouldUpdate(changedProps): boolean {
if (changedProps.has("deviceId") || changedProps.has("automations")) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
return true;
}
return false;
}
protected render() {
if (this.automations.length === 0 || !this.entityReg) {
return nothing;
}
const automations = this._showSecondary
? this.automations
: this.automations.filter(
(automation) => automation.metadata?.secondary === false
);
return html`
<h3>${this.hass.localize(this.headerKey)}</h3>
<div class="content">
<ha-chip-set>
${automations.map(
(automation, idx) => html`
<ha-assist-chip
filled
.index=${idx}
@click=${this._handleAutomationClicked}
class=${automation.metadata?.secondary ? "secondary" : ""}
.label=${this._localizeDeviceAutomation(
this.hass,
this.entityReg!,
automation
)}
>
</ha-assist-chip>
`
)}
</ha-chip-set>
${!this._showSecondary && automations.length < this.automations.length
? html`<button class="link" @click=${this._toggleSecondary}>
Show ${this.automations.length - automations.length} more...
</button>`
: ""}
</div>
`;
}
private _toggleSecondary() {
this._showSecondary = !this._showSecondary;
}
private _handleAutomationClicked(ev: CustomEvent) {
const automation = { ...this.automations[(ev.currentTarget as any).index] };
if (!automation) {
return;
}
delete automation.metadata;
if (this.script) {
showScriptEditor({ sequence: [automation as DeviceAction] });
fireEvent(this, "entry-selected");
return;
}
const data = {};
data[this.type] = [automation];
showAutomationEditor(data);
fireEvent(this, "entry-selected");
}
static styles = [
buttonLinkStyle,
css`
h3 {
color: var(--primary-text-color);
}
.secondary {
--ha-assist-chip-filled-container-color: rgba(
var(--rgb-primary-text-color),
0.07
);
}
button.link {
color: var(--primary-color);
}
`,
];
}
@@ -1,8 +1,18 @@
import "@material/mwc-button/mwc-button";
import { CSSResultGroup, html, LitElement, nothing } from "lit";
import {
mdiAbTesting,
mdiGestureTap,
mdiPencilOutline,
mdiRoomService,
} 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-dialog";
import { shouldHandleRequestSelectedEvent } from "../../../../common/mwc/handle-request-selected-event";
import { createCloseHeading } from "../../../../components/ha-dialog";
import {
AutomationConfig,
showAutomationEditor,
} from "../../../../data/automation";
import {
DeviceAction,
DeviceCondition,
@@ -12,11 +22,9 @@ import {
fetchDeviceTriggers,
sortDeviceAutomations,
} from "../../../../data/device_automation";
import { haStyleDialog } from "../../../../resources/styles";
import { ScriptConfig, showScriptEditor } from "../../../../data/script";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import "./ha-device-actions-card";
import "./ha-device-conditions-card";
import "./ha-device-triggers-card";
import { DeviceAutomationDialogParams } from "./show-dialog-device-automation";
@customElement("dialog-device-automation")
@@ -77,75 +85,184 @@ export class DialogDeviceAutomation extends LitElement {
});
}
private _handleRowClick = (ev) => {
if (!shouldHandleRequestSelectedEvent(ev) || !this._params) {
return;
}
const type = (ev.currentTarget as any).type;
const isScript = this._params.script;
this.closeDialog();
if (isScript) {
const newScript = {} as ScriptConfig;
if (type === "action") {
newScript.sequence = [this._actions[0]];
}
showScriptEditor(newScript, true);
} else {
const newAutomation = {} as AutomationConfig;
if (type === "trigger") {
newAutomation.triggers = [this._triggers[0]];
}
if (type === "condition") {
newAutomation.conditions = [this._conditions[0]];
}
if (type === "action") {
newAutomation.actions = [this._actions[0]];
}
showAutomationEditor(newAutomation, true);
}
};
protected render() {
if (!this._params) {
return nothing;
}
const mode = this._params.script ? "script" : "automation";
const title = this.hass.localize(`ui.panel.config.devices.${mode}.create`, {
type: this.hass.localize(
`ui.panel.config.devices.type.${
this._params.device.entry_type || "device"
}`
),
});
return html`
<ha-dialog
open
hideActions
@closed=${this.closeDialog}
.heading=${this.hass.localize(
`ui.panel.config.devices.${
this._params.script ? "script" : "automation"
}.create`,
{
type: this.hass.localize(
`ui.panel.config.devices.type.${
this._params.device.entry_type || "device"
}`
),
}
)}
.heading=${createCloseHeading(this.hass, title)}
>
<div @entry-selected=${this.closeDialog}>
<mwc-list
innerRole="listbox"
itemRoles="option"
innerAriaLabel="Create new automation"
rootTabbable
dialogInitialFocus
>
${this._triggers.length
? html`
<ha-list-item
hasmeta
twoline
graphic="icon"
.type=${"trigger"}
@request-selected=${this._handleRowClick}
>
<ha-svg-icon
slot="graphic"
.path=${mdiGestureTap}
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.devices.automation.triggers.title`
)}
<span slot="secondary">
${this.hass.localize(
`ui.panel.config.devices.automation.triggers.description`
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
: nothing}
${this._conditions.length
? html`
<ha-list-item
hasmeta
twoline
graphic="icon"
.type=${"condition"}
@request-selected=${this._handleRowClick}
>
<ha-svg-icon
slot="graphic"
.path=${mdiAbTesting}
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.devices.automation.conditions.title`
)}
<span slot="secondary">
${this.hass.localize(
`ui.panel.config.devices.automation.conditions.description`
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
: nothing}
${this._actions.length
? html`
<ha-list-item
hasmeta
twoline
graphic="icon"
.type=${"action"}
@request-selected=${this._handleRowClick}
>
<ha-svg-icon
slot="graphic"
.path=${mdiRoomService}
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.devices.${mode}.actions.title`
)}
<span slot="secondary">
${this.hass.localize(
`ui.panel.config.devices.${mode}.actions.description`
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
: nothing}
${this._triggers.length ||
this._conditions.length ||
this._actions.length
? html`
${this._triggers.length
? html`
<ha-device-triggers-card
.hass=${this.hass}
.automations=${this._triggers}
.entityReg=${this._params.entityReg}
></ha-device-triggers-card>
`
: ""}
${this._conditions.length
? html`
<ha-device-conditions-card
.hass=${this.hass}
.automations=${this._conditions}
.entityReg=${this._params.entityReg}
></ha-device-conditions-card>
`
: ""}
${this._actions.length
? html`
<ha-device-actions-card
.hass=${this.hass}
.automations=${this._actions}
.script=${this._params.script}
.entityReg=${this._params.entityReg}
></ha-device-actions-card>
`
: ""}
`
: this.hass.localize(
"ui.panel.config.devices.automation.no_device_automations"
? html`<li divider role="separator"></li>`
: nothing}
<ha-list-item
hasmeta
twoline
graphic="icon"
@request-selected=${this._handleRowClick}
>
<ha-svg-icon slot="graphic" .path=${mdiPencilOutline}></ha-svg-icon>
${this.hass.localize(`ui.panel.config.devices.${mode}.new.title`)}
<span slot="secondary">
${this.hass.localize(
`ui.panel.config.devices.${mode}.new.description`
)}
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</mwc-button>
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</mwc-list>
</ha-dialog>
`;
}
static get styles(): CSSResultGroup {
return haStyleDialog;
return [
haStyle,
haStyleDialog,
css`
ha-dialog {
--dialog-content-padding: 0;
--mdc-dialog-max-height: 60vh;
}
@media all and (min-width: 550px) {
ha-dialog {
--mdc-dialog-min-width: 500px;
}
}
ha-icon-next {
width: 24px;
}
`,
];
}
}
@@ -1,23 +0,0 @@
import { customElement } from "lit/decorators";
import {
DeviceCondition,
localizeDeviceAutomationCondition,
} from "../../../../data/device_automation";
import { HaDeviceAutomationCard } from "./ha-device-automation-card";
@customElement("ha-device-conditions-card")
export class HaDeviceConditionsCard extends HaDeviceAutomationCard<DeviceCondition> {
readonly type = "condition";
readonly headerKey = "ui.panel.config.devices.automation.conditions.caption";
constructor() {
super(localizeDeviceAutomationCondition);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-device-conditions-card": HaDeviceConditionsCard;
}
}
@@ -1,23 +0,0 @@
import { customElement } from "lit/decorators";
import {
DeviceTrigger,
localizeDeviceAutomationTrigger,
} from "../../../../data/device_automation";
import { HaDeviceAutomationCard } from "./ha-device-automation-card";
@customElement("ha-device-triggers-card")
export class HaDeviceTriggersCard extends HaDeviceAutomationCard<DeviceTrigger> {
readonly type = "trigger";
readonly headerKey = "ui.panel.config.devices.automation.triggers.caption";
constructor() {
super(localizeDeviceAutomationTrigger);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-device-triggers-card": HaDeviceTriggersCard;
}
}
@@ -37,6 +37,7 @@ import {
ThreadDataSet,
ThreadRouter,
addThreadDataSet,
getThreadDataSetTLV,
listThreadDataSets,
removeThreadDataSet,
setPreferredBorderAgent,
@@ -168,8 +169,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
(otbr) => otbr.extended_pan_id === network.dataset!.extended_pan_id
));
const canImportKeychain =
this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain &&
otbrForNetwork;
this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain;
return html`<ha-card>
<div class="card-header">
@@ -208,8 +208,12 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
${network.routers.map((router) => {
const otbr =
this._otbrInfo && this._otbrInfo[router.extended_address];
const showOverflow =
("dataset" in network && router.border_agent_id) || otbr;
const showDefaultRouter = !!network.dataset;
const isDefaultRouter =
showDefaultRouter &&
router.extended_address ===
network.dataset!.preferred_extended_address;
const showOverflow = showDefaultRouter || otbr;
return html`<ha-list-item
class="router"
twoline
@@ -235,9 +239,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
""}
<span slot="secondary">${router.server}</span>
${showOverflow
? html`${network.dataset &&
router.extended_address ===
network.dataset.preferred_extended_address
? html`${isDefaultRouter
? html`<ha-svg-icon
.path=${mdiCellphoneKey}
.title=${this.hass.localize(
@@ -259,13 +261,9 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
${network.dataset && router.border_agent_id
? html`<ha-list-item
.disabled=${router.border_agent_id ===
network.dataset.preferred_border_agent_id}
>
${router.border_agent_id ===
network.dataset.preferred_border_agent_id
${showDefaultRouter
? html`<ha-list-item .disabled=${isDefaultRouter}>
${isDefaultRouter
? this.hass.localize(
"ui.panel.config.thread.default_router"
)
@@ -321,9 +319,13 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
>
</div>`
: ""}
${canImportKeychain
${canImportKeychain &&
network.dataset?.preferred &&
network.routers?.length
? html`<div class="card-actions">
<mwc-button .otbr=${otbrForNetwork} @click=${this._sendCredentials}
<mwc-button
.networkDataset=${network.dataset}
@click=${this._sendCredentials}
>Send credentials to phone</mwc-button
>
</div>`
@@ -331,17 +333,30 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
</ha-card>`;
}
private _sendCredentials(ev) {
const otbr = (ev.currentTarget as any).otbr as OTBRInfo;
if (!otbr) {
private async _sendCredentials(ev) {
const dataset = (ev.currentTarget as any).networkDataset as ThreadDataSet;
if (!dataset) {
return;
}
if (
!dataset.preferred_extended_address &&
!dataset.preferred_border_agent_id
) {
showAlertDialog(this, {
title: "Error",
text: this.hass.localize("ui.panel.config.thread.no_preferred_router"),
});
return;
}
this.hass.auth.external!.fireMessage({
type: "thread/store_in_platform_keychain",
payload: {
mac_extended_address: otbr.extended_address,
border_agent_id: otbr.border_agent_id,
active_operational_dataset: otbr.active_dataset_tlvs,
mac_extended_address: dataset.preferred_extended_address,
border_agent_id: dataset.preferred_border_agent_id,
active_operational_dataset: (
await getThreadDataSetTLV(this.hass, dataset.dataset_id)
).tlv,
extended_pan_id: dataset.extended_pan_id,
},
});
}
@@ -467,10 +482,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
const network = (ev.currentTarget as any).network as ThreadNetwork;
const router = (ev.currentTarget as any).router as ThreadRouter;
const otbr = (ev.currentTarget as any).otbr as OTBRInfo;
const index =
network.dataset && router.border_agent_id
? Number(ev.detail.index)
: Number(ev.detail.index) + 1;
const index = Number(ev.detail.index);
switch (index) {
case 0:
this._setPreferredBorderAgent(network.dataset!, router);
@@ -24,6 +24,7 @@ import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant, Route } from "../../../../../types";
import { formatAsPaddedHex, sortZHAGroups } from "./functions";
import { zhaTabs } from "./zha-config-dashboard";
import { LocalizeFunc } from "../../../../../common/translations/localize";
export interface GroupRowData extends ZHAGroup {
group?: GroupRowData;
@@ -71,38 +72,35 @@ export class ZHAGroupsDashboard extends LitElement {
});
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer<GroupRowData> =>
narrow
? {
name: {
title: "Group",
sortable: true,
filterable: true,
direction: "asc",
flex: 2,
},
}
: {
name: {
title: this.hass.localize("ui.panel.config.zha.groups.groups"),
sortable: true,
filterable: true,
direction: "asc",
flex: 2,
},
group_id: {
title: this.hass.localize("ui.panel.config.zha.groups.group_id"),
type: "numeric",
template: (group) => html` ${formatAsPaddedHex(group.group_id)} `,
sortable: true,
},
members: {
title: this.hass.localize("ui.panel.config.zha.groups.members"),
type: "numeric",
template: (group) => html` ${group.members.length} `,
sortable: true,
},
}
(localize: LocalizeFunc): DataTableColumnContainer => {
const columns: DataTableColumnContainer<GroupRowData> = {
name: {
title: localize("ui.panel.config.zha.groups.groups"),
sortable: true,
filterable: true,
showNarrow: true,
main: true,
hideable: false,
moveable: false,
direction: "asc",
flex: 2,
},
group_id: {
title: localize("ui.panel.config.zha.groups.group_id"),
type: "numeric",
template: (group) => html` ${formatAsPaddedHex(group.group_id)} `,
sortable: true,
},
members: {
title: localize("ui.panel.config.zha.groups.members"),
type: "numeric",
template: (group) => html` ${group.members.length} `,
sortable: true,
},
};
return columns;
}
);
protected render(): TemplateResult {
@@ -112,7 +110,7 @@ export class ZHAGroupsDashboard extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.columns=${this._columns(this.narrow)}
.columns=${this._columns(this.hass.localize)}
.data=${this._formattedGroups(this._groups)}
@row-click=${this._handleRowClicked}
clickable
@@ -47,7 +47,7 @@ export const showRepairsFlowDialog = (
},
{
flowType: "repair_flow",
loadDevicesAndAreas: false,
showDevices: false,
createFlow: async (hass, handler) => {
const [step] = await Promise.all([
createRepairsFlow(hass, handler, issue.issue_id),
@@ -1,8 +1,20 @@
import "@material/mwc-button/mwc-button";
import { mdiHelpCircle } from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
import {
extractSearchParam,
removeSearchParam,
} from "../../../common/url/search-params";
import { nestedArrayMove } from "../../../common/util/array-move";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
@@ -12,6 +24,7 @@ import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import "../automation/action/ha-automation-action";
import type HaAutomationAction from "../automation/action/ha-automation-action";
import "./ha-script-fields";
import type HaScriptFields from "./ha-script-fields";
@@ -58,6 +71,31 @@ export class HaManualScriptEditor extends LitElement {
}
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
const expanded = extractSearchParam("expanded");
if (expanded === "1") {
this._clearParam("expanded");
const items = this.shadowRoot!.querySelectorAll<HaAutomationAction>(
"ha-automation-action"
);
items.forEach((el) => {
el.updateComplete.then(() => {
el.expandAll();
});
});
}
}
private _clearParam(param: string) {
window.history.replaceState(
null,
"",
constructUrlCurrentPath(removeSearchParam(param))
);
}
protected render() {
return html`
${this.config.description
@@ -7,6 +7,7 @@ import "../../../components/ha-card";
import "../../../components/ha-textfield";
import "../../../components/ha-yaml-editor";
import "../../../components/ha-button";
import "../../../components/ha-alert";
import { HomeAssistant } from "../../../types";
@customElement("event-subscribe-card")
@@ -22,6 +23,8 @@ class EventSubscribeCard extends LitElement {
event: HassEvent;
}> = [];
@state() private _error?: string;
private _eventCount = 0;
public disconnectedCallback() {
@@ -52,6 +55,9 @@ class EventSubscribeCard extends LitElement {
.value=${this._eventType}
@input=${this._valueChanged}
></ha-textfield>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
</div>
<div class="card-actions">
<ha-button
@@ -110,33 +116,43 @@ class EventSubscribeCard extends LitElement {
private _valueChanged(ev): void {
this._eventType = ev.target.value;
this._error = undefined;
}
private async _startOrStopListening(): Promise<void> {
if (this._subscribed) {
this._subscribed();
this._subscribed = undefined;
this._error = undefined;
} else {
this._subscribed = await this.hass!.connection.subscribeEvents<HassEvent>(
(event) => {
const tail =
this._events.length > 30 ? this._events.slice(0, 29) : this._events;
this._events = [
{
event,
id: this._eventCount++,
},
...tail,
];
},
this._eventType
);
try {
this._subscribed =
await this.hass!.connection.subscribeEvents<HassEvent>((event) => {
const tail =
this._events.length > 30
? this._events.slice(0, 29)
: this._events;
this._events = [
{
event,
id: this._eventCount++,
},
...tail,
];
}, this._eventType);
} catch (error: any) {
this._error = this.hass!.localize(
"ui.panel.developer-tools.tabs.events.subscribe_failed",
{ error: error.message || "Unknown error" }
);
}
}
}
private _clearEvents(): void {
this._events = [];
this._eventCount = 0;
this._error = undefined;
}
static get styles(): CSSResultGroup {
@@ -145,6 +161,9 @@ class EventSubscribeCard extends LitElement {
display: block;
margin-bottom: 16px;
}
.error-message {
margin-top: 8px;
}
.event {
border-top: 1px solid var(--divider-color);
padding-top: 8px;
@@ -1,24 +1,51 @@
import "@material/mwc-button/mwc-button";
import { mdiSlopeUphill } from "@mdi/js";
import {
mdiArrowDown,
mdiArrowUp,
mdiClose,
mdiCog,
mdiFormatListChecks,
mdiMenuDown,
mdiSlopeUphill,
mdiUnfoldLessHorizontal,
mdiUnfoldMoreHorizontal,
} from "@mdi/js";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/chips/ha-assist-chip";
import "../../../components/data-table/ha-data-table";
import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import type {
DataTableColumnContainer,
HaDataTable,
SelectionChangedEvent,
SortingDirection,
} from "../../../components/data-table/ha-data-table";
import { showDataTableSettingsDialog } from "../../../components/data-table/show-dialog-data-table-settings";
import "../../../components/ha-md-button-menu";
import "../../../components/ha-dialog";
import { HaMenu } from "../../../components/ha-menu";
import "../../../components/ha-md-menu-item";
import "../../../components/search-input-outlined";
import { subscribeEntityRegistry } from "../../../data/entity_registry";
import {
getStatisticIds,
StatisticsMetaData,
StatisticsValidationResult,
clearStatistics,
getStatisticIds,
updateStatisticsIssues,
validateStatistics,
} from "../../../data/recorder";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { showConfirmationDialog } from "../../lovelace/custom-card-helpers";
import { fixStatisticsIssue } from "./fix-statistics";
import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
@@ -30,9 +57,17 @@ const FIX_ISSUES_ORDER = {
units_changed: 3,
};
const FIXABLE_ISSUES = [
"no_state",
"entity_no_longer_recorded",
"unsupported_state_class",
"units_changed",
];
type StatisticData = StatisticsMetaData & {
issues?: StatisticsValidationResult[];
state?: HassEntity;
selectable?: boolean;
};
type DisplayedStatisticData = StatisticData & {
@@ -48,8 +83,40 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
@state() private _data: StatisticData[] = [] as StatisticsMetaData[];
@state() private filter = "";
@state() private _selected: string[] = [];
@state() private groupOrder?: string[];
@state() private columnOrder?: string[];
@state() private hiddenColumns?: string[];
@state() private _sortColumn?: string;
@state() private _sortDirection: SortingDirection = null;
@state() private _groupColumn?: string;
@state() private _selectMode = false;
@query("ha-data-table", true) private _dataTable!: HaDataTable;
@query("#group-by-menu") private _groupByMenu!: HaMenu;
@query("#sort-by-menu") private _sortByMenu!: HaMenu;
private _disabledEntities = new Set<string>();
private _toggleGroupBy() {
this._groupByMenu.open = !this._groupByMenu.open;
}
private _toggleSortBy() {
this._sortByMenu.open = !this._sortByMenu.open;
}
protected firstUpdated() {
this._validateStatistics();
}
@@ -108,6 +175,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
),
sortable: true,
filterable: true,
groupable: true,
},
issues_string: {
title: localize(
@@ -115,6 +183,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
),
sortable: true,
filterable: true,
groupable: true,
direction: "asc",
flex: 2,
template: (statistic) =>
@@ -133,7 +202,11 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
.data=${statistic.issues}
>
${localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.fix"
statistic.issues.some((issue) =>
FIXABLE_ISSUES.includes(issue.type)
)
? "ui.panel.developer-tools.tabs.statistics.fix_issue.fix"
: "ui.panel.developer-tools.tabs.statistics.fix_issue.info"
)}
</mwc-button>`
: "—"}`,
@@ -164,22 +237,367 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
);
protected render() {
const localize = this.hass.localize;
const columns = this._columns(this.hass.localize);
const selectModeBtn = !this._selectMode
? html`<ha-assist-chip
class="has-dropdown select-mode-chip"
.active=${this._selectMode}
@click=${this._enableSelectMode}
.title=${localize(
"ui.components.subpage-data-table.enter_selection_mode"
)}
>
<ha-svg-icon slot="icon" .path=${mdiFormatListChecks}></ha-svg-icon>
</ha-assist-chip> `
: nothing;
const searchBar = html`<search-input-outlined
.hass=${this.hass}
.filter=${this.filter}
@value-changed=${this._handleSearchChange}
>
</search-input-outlined>`;
const sortByMenu = Object.values(columns).find((col) => col.sortable)
? html`
<ha-assist-chip
.label=${localize("ui.components.subpage-data-table.sort_by", {
sortColumn: this._sortColumn
? ` ${columns[this._sortColumn]?.title || columns[this._sortColumn]?.label}` ||
""
: "",
})}
id="sort-by-anchor"
@click=${this._toggleSortBy}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
`
: nothing;
const groupByMenu = Object.values(columns).find((col) => col.groupable)
? html`
<ha-assist-chip
.label=${localize("ui.components.subpage-data-table.group_by", {
groupColumn: this._groupColumn
? ` ${columns[this._groupColumn].title || columns[this._groupColumn].label}`
: "",
})}
id="group-by-anchor"
@click=${this._toggleGroupBy}
>
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon
></ha-assist-chip>
`
: nothing;
const settingsButton = html`<ha-assist-chip
class="has-dropdown select-mode-chip"
@click=${this._openSettings}
.title=${localize("ui.components.subpage-data-table.settings")}
>
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
</ha-assist-chip>`;
return html`
<ha-data-table
.hass=${this.hass}
.columns=${this._columns(this.hass.localize)}
.data=${this._displayData(this._data, this.hass.localize)}
.noDataText=${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.data_table.no_statistics"
<div>
${this._selectMode
? html`<div class="selection-bar">
<div class="selection-controls">
<ha-icon-button
.path=${mdiClose}
@click=${this._disableSelectMode}
.label=${localize(
"ui.components.subpage-data-table.exit_selection_mode"
)}
></ha-icon-button>
<ha-md-button-menu positioning="absolute">
<ha-assist-chip
.label=${localize(
"ui.components.subpage-data-table.select"
)}
slot="trigger"
>
<ha-svg-icon
slot="icon"
.path=${mdiFormatListChecks}
></ha-svg-icon>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
<ha-md-menu-item
.value=${undefined}
@click=${this._selectAll}
>
<div slot="headline">
${localize("ui.components.subpage-data-table.select_all")}
</div>
</ha-md-menu-item>
<ha-md-menu-item
.value=${undefined}
@click=${this._selectAllIssues}
>
<div slot="headline">
${localize(
"ui.panel.developer-tools.tabs.statistics.data_table.select_all_issues"
)}
</div>
</ha-md-menu-item>
<ha-md-menu-item
.value=${undefined}
@click=${this._selectNone}
>
<div slot="headline">
${localize(
"ui.components.subpage-data-table.select_none"
)}
</div>
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-md-menu-item
.value=${undefined}
@click=${this._disableSelectMode}
>
<div slot="headline">
${localize(
"ui.components.subpage-data-table.close_select_mode"
)}
</div>
</ha-md-menu-item>
</ha-md-button-menu>
<p>
${localize("ui.components.subpage-data-table.selected", {
selected: this._selected.length,
})}
</p>
</div>
<div class="center-vertical">
<slot name="selection-bar"></slot>
</div>
<ha-assist-chip
.label=${localize(
"ui.panel.developer-tools.tabs.statistics.delete_selected"
)}
.disabled=${!this._selected.length}
@click=${this._clearSelected}
>
</ha-assist-chip>
</div>`
: nothing}
<div slot="toolbar-icon">
<slot name="toolbar-icon"></slot>
</div>
${this.narrow
? html`
<div slot="header">
<slot name="header">
<div class="search-toolbar">${searchBar}</div>
</slot>
</div>
`
: ""}
<ha-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.columns=${columns}
.data=${this._displayData(this._data, this.hass.localize)}
.noDataText=${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.data_table.no_statistics"
)}
.filter=${this.filter}
.selectable=${this._selectMode}
id="statistic_id"
clickable
.sortColumn=${this._sortColumn}
.sortDirection=${this._sortDirection}
.groupColumn=${this._groupColumn}
.groupOrder=${this.groupOrder}
.columnOrder=${this.columnOrder}
.hiddenColumns=${this.hiddenColumns}
@row-click=${this._rowClicked}
@selection-changed=${this._handleSelectionChanged}
>
${!this.narrow
? html`
<div slot="header">
<slot name="header">
<div class="table-header">
${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu}${settingsButton}
</div>
</slot>
</div>
`
: html`<div slot="header"></div>
<div slot="header-row" class="narrow-header-row">
${selectModeBtn}${groupByMenu}${sortByMenu}${settingsButton}
</div>`}
</ha-data-table>
</div>
<ha-menu anchor="group-by-anchor" id="group-by-menu" positioning="fixed">
${Object.entries(columns).map(([id, column]) =>
column.groupable
? html`
<ha-md-menu-item
.value=${id}
@click=${this._handleGroupBy}
.selected=${id === this._groupColumn}
class=${classMap({ selected: id === this._groupColumn })}
>
${column.title || column.label}
</ha-md-menu-item>
`
: nothing
)}
.narrow=${this.narrow}
id="statistic_id"
clickable
@row-click=${this._rowClicked}
></ha-data-table>
<ha-md-menu-item
.value=${undefined}
@click=${this._handleGroupBy}
.selected=${this._groupColumn === undefined}
class=${classMap({ selected: this._groupColumn === undefined })}
>
${localize("ui.components.subpage-data-table.dont_group_by")}
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-md-menu-item
@click=${this._collapseAllGroups}
.disabled=${this._groupColumn === undefined}
>
<ha-svg-icon
slot="start"
.path=${mdiUnfoldLessHorizontal}
></ha-svg-icon>
${localize("ui.components.subpage-data-table.collapse_all_groups")}
</ha-md-menu-item>
<ha-md-menu-item
@click=${this._expandAllGroups}
.disabled=${this._groupColumn === undefined}
>
<ha-svg-icon
slot="start"
.path=${mdiUnfoldMoreHorizontal}
></ha-svg-icon>
${localize("ui.components.subpage-data-table.expand_all_groups")}
</ha-md-menu-item>
</ha-menu>
<ha-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed">
${Object.entries(columns).map(([id, column]) =>
column.sortable
? html`
<ha-md-menu-item
.value=${id}
@click=${this._handleSortBy}
keep-open
.selected=${id === this._sortColumn}
class=${classMap({ selected: id === this._sortColumn })}
>
${this._sortColumn === id
? html`
<ha-svg-icon
slot="end"
.path=${this._sortDirection === "desc"
? mdiArrowDown
: mdiArrowUp}
></ha-svg-icon>
`
: nothing}
${column.title || column.label}
</ha-md-menu-item>
`
: nothing
)}
</ha-menu>
`;
}
private _handleSearchChange(ev: CustomEvent) {
if (this.filter === ev.detail.value) {
return;
}
this.filter = ev.detail.value;
}
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selected = ev.detail.value;
}
private _handleSortBy(ev) {
const columnId = ev.currentTarget.value;
if (!this._sortDirection || this._sortColumn !== columnId) {
this._sortDirection = "asc";
} else if (this._sortDirection === "asc") {
this._sortDirection = "desc";
} else {
this._sortDirection = null;
}
this._sortColumn = this._sortDirection === null ? undefined : columnId;
}
private _handleGroupBy(ev) {
this._setGroupColumn(ev.currentTarget.value);
}
private _setGroupColumn(columnId: string) {
this._groupColumn = columnId;
}
private _openSettings() {
showDataTableSettingsDialog(this, {
columns: this._columns(this.hass.localize),
hiddenColumns: this.hiddenColumns,
columnOrder: this.columnOrder,
onUpdate: (
columnOrder: string[] | undefined,
hiddenColumns: string[] | undefined
) => {
this.columnOrder = columnOrder;
this.hiddenColumns = hiddenColumns;
},
localizeFunc: this.hass.localize,
});
}
private _collapseAllGroups() {
this._dataTable.collapseAllGroups();
}
private _expandAllGroups() {
this._dataTable.expandAllGroups();
}
private _enableSelectMode() {
this._selectMode = true;
}
private _disableSelectMode() {
this._selectMode = false;
this._dataTable.clearSelection();
}
private _selectAll() {
this._dataTable.selectAll();
}
private _selectNone() {
this._dataTable.clearSelection();
}
private _selectAllIssues() {
this._dataTable.select(
this._data
.filter((statistic) => statistic.issues)
.map((statistic) => statistic.statistic_id),
true
);
}
private _showStatisticsAdjustSumDialog(ev) {
ev.stopPropagation();
showStatisticsAdjustSumDialog(this, {
@@ -219,6 +637,8 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
validateStatistics(this.hass),
]);
updateStatisticsIssues(this.hass);
const statsIds = new Set();
this._data = statisticIds
@@ -253,6 +673,31 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
});
}
private _clearSelected = async () => {
if (!this._selected.length) {
return;
}
const deletableIds = this._selected;
await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.multi_delete.title"
),
text: html`${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.multi_delete.info_text",
{ statistic_count: deletableIds.length }
)}`,
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(this.hass, deletableIds);
this._validateStatistics();
this._dataTable.clearSelection();
},
});
};
private _fixIssue = async (ev) => {
const issues = (ev.currentTarget.data as StatisticsValidationResult[]).sort(
(itemA, itemB) =>
@@ -265,7 +710,125 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
};
static get styles(): CSSResultGroup {
return haStyle;
return [
haStyle,
css`
:host {
display: block;
height: 100%;
}
ha-data-table {
width: 100%;
height: 100%;
--data-table-border-width: 0;
}
:host(:not([narrow])) ha-data-table {
height: calc(100vh - 1px - var(--header-height));
display: block;
}
:host([narrow]) {
--expansion-panel-summary-padding: 0 16px;
}
.table-header {
display: flex;
align-items: center;
--mdc-shape-small: 0;
height: 56px;
width: 100%;
justify-content: space-between;
padding: 0 16px;
gap: 16px;
box-sizing: border-box;
background: var(--primary-background-color);
border-bottom: 1px solid var(--divider-color);
}
search-input-outlined {
flex: 1;
}
.search-toolbar {
display: flex;
align-items: center;
color: var(--secondary-text-color);
}
.narrow-header-row {
display: flex;
align-items: center;
gap: 16px;
padding: 0 16px;
overflow-x: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
}
.selection-bar {
background: rgba(var(--rgb-primary-color), 0.1);
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
box-sizing: border-box;
font-size: 14px;
--ha-assist-chip-container-color: var(--card-background-color);
}
.selection-controls {
display: flex;
align-items: center;
gap: 8px;
}
.selection-controls p {
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
}
.center-vertical {
display: flex;
align-items: center;
gap: 8px;
}
.relative {
position: relative;
}
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
--ha-assist-chip-container-color: var(--card-background-color);
}
.select-mode-chip {
--md-assist-chip-icon-label-space: 0;
--md-assist-chip-trailing-space: 8px;
}
ha-dialog {
--mdc-dialog-min-width: calc(
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
);
--mdc-dialog-max-width: calc(
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
);
--mdc-dialog-min-height: 100%;
--mdc-dialog-max-height: 100%;
--vertical-align-dialog: flex-end;
--ha-dialog-border-radius: 0;
--dialog-content-padding: 0;
}
#sort-by-anchor,
#group-by-anchor,
ha-button-menu-new ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
`,
];
}
}
@@ -8,6 +8,7 @@ import {
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-sortable";
@@ -124,7 +125,7 @@ export class HuiViewBadges extends LitElement {
.options=${BADGE_SORTABLE_OPTIONS}
invert-swap
>
<div class="badges">
<div class="badges ${classMap({ "edit-mode": editMode })}">
${repeat(
badges,
(badge) => this._getBadgeKey(badge),
@@ -185,6 +186,8 @@ export class HuiViewBadges extends LitElement {
hui-badge-edit-mode {
display: block;
position: relative;
min-width: 36px;
min-height: 36px;
}
.add {
@@ -129,6 +129,8 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
return css`
ha-card {
background: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
border: none;
box-shadow: none;
padding: 0;
@@ -64,11 +64,16 @@ const addEntities = (entities: Set<string>, obj) => {
if (obj.badges && Array.isArray(obj.badges)) {
obj.badges.forEach((badge) => addEntityId(entities, badge));
}
if (obj.sections && Array.isArray(obj.sections)) {
obj.sections.forEach((section) => addEntities(entities, section));
}
};
export const computeUsedEntities = (config: LovelaceConfig): Set<string> => {
const entities = new Set<string>();
config.views.forEach((view) => addEntities(entities, view));
config.views.forEach((view) => {
addEntities(entities, view);
});
return entities;
};
+15 -6
View File
@@ -3,6 +3,7 @@ import { navigate } from "../../../common/navigate";
import { forwardHaptic } from "../../../data/haptics";
import { domainToName } from "../../../data/integration";
import { ActionConfig } from "../../../data/lovelace/config/action";
import { callExecuteScript } from "../../../data/service";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { showVoiceCommandDialog } from "../../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import { HomeAssistant } from "../../../types";
@@ -94,12 +95,13 @@ export const handleAction = async (
switch (actionConfig.action) {
case "more-info": {
if (config.entity || config.camera_image || config.image_entity) {
fireEvent(node, "hass-more-info", {
entityId: (config.entity ||
config.camera_image ||
config.image_entity)!,
});
const entityId =
actionConfig.entity_id ||
config.entity ||
config.camera_image ||
config.image_entity;
if (entityId) {
fireEvent(node, "hass-more-info", { entityId });
} else {
showToast(node, {
message: hass.localize(
@@ -176,6 +178,13 @@ export const handleAction = async (
});
break;
}
case "sequence": {
if (!actionConfig.actions) {
return;
}
callExecuteScript(hass, actionConfig.actions);
break;
}
case "fire-dom-event": {
fireEvent(node, "ll-custom", actionConfig);
}
@@ -1,3 +1,5 @@
import { ContextProvider } from "@lit-labs/context";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
@@ -14,7 +16,10 @@ import "../../../components/ha-assist-pipeline-picker";
import { HaFormSchema, SchemaUnion } from "../../../components/ha-form/types";
import "../../../components/ha-help-tooltip";
import "../../../components/ha-navigation-picker";
import { HaSelect } from "../../../components/ha-select";
import "../../../components/ha-service-control";
import { fullEntitiesContext } from "../../../data/context";
import { subscribeEntityRegistry } from "../../../data/entity_registry";
import {
ActionConfig,
CallServiceActionConfig,
@@ -22,9 +27,9 @@ import {
UrlActionConfig,
} from "../../../data/lovelace/config/action";
import { ServiceAction } from "../../../data/script";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../types";
import { EditorTarget } from "../editor/types";
import { HaSelect } from "../../../components/ha-select";
export type UiAction = Exclude<ActionConfig["action"], "fire-dom-event">;
@@ -34,6 +39,7 @@ const DEFAULT_ACTIONS: UiAction[] = [
"navigate",
"url",
"perform-action",
"sequence",
"assist",
"none",
];
@@ -70,8 +76,17 @@ const ASSIST_SCHEMA = [
},
] as const satisfies readonly HaFormSchema[];
const SEQUENCE_SCHEMA = [
{
name: "actions",
selector: {
action: {},
},
},
] as const satisfies readonly HaFormSchema[];
@customElement("hui-action-editor")
export class HuiActionEditor extends LitElement {
export class HuiActionEditor extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public config?: ActionConfig;
@property() public label?: string;
@@ -86,6 +101,19 @@ export class HuiActionEditor extends LitElement {
@query("ha-select") private _select!: HaSelect;
private _entitiesContext = new ContextProvider(this, {
context: fullEntitiesContext,
initialValue: [],
});
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass!.connection!, (entities) => {
this._entitiesContext.setValue(entities);
}),
];
}
get _navigation_path(): string {
const config = this.config as NavigateActionConfig | undefined;
return config?.navigation_path || "";
@@ -120,6 +148,11 @@ export class HuiActionEditor extends LitElement {
}
}
protected firstUpdated(_changedProperties: PropertyValues): void {
this.hass!.loadFragmentTranslation("config");
this.hass!.loadBackendTranslation("device_automation");
}
protected render() {
if (!this.hass) {
return nothing;
@@ -218,6 +251,17 @@ export class HuiActionEditor extends LitElement {
</ha-form>
`
: nothing}
${this.config?.action === "sequence"
? html`
<ha-form
.hass=${this.hass}
.schema=${SEQUENCE_SCHEMA}
.data=${this.config}
.computeLabel=${this._computeFormLabel}
@value-changed=${this._formValueChanged}
></ha-form>
`
: nothing}
`;
}
@@ -289,7 +333,15 @@ export class HuiActionEditor extends LitElement {
});
}
private _computeFormLabel(schema: SchemaUnion<typeof ASSIST_SCHEMA>) {
private _computeFormLabel(
schema:
| SchemaUnion<typeof ASSIST_SCHEMA>
| SchemaUnion<typeof NAVIGATE_SCHEMA>
| SchemaUnion<typeof SEQUENCE_SCHEMA>
) {
if (schema.name === "actions") {
return "";
}
return this.hass?.localize(
`ui.panel.lovelace.editor.action-editor.${schema.name}`
);
@@ -36,7 +36,7 @@ export const DEFAULT_CONFIG: Partial<EntityHeadingBadgeConfig> = {
const entityConfigStruct = object({
type: optional(string()),
entity: string(),
entity: optional(string()),
name: optional(string()),
icon: optional(string()),
state_content: optional(union([string(), array(string())])),
@@ -48,6 +48,12 @@ const actionConfigStructService = object({
confirmation: optional(actionConfigStructConfirmation),
});
const actionConfigStructSequence = object({
action: literal("sequence"),
actions: optional(array(object())),
confirmation: optional(actionConfigStructConfirmation),
});
const actionConfigStructNavigate = object({
action: literal("navigate"),
navigation_path: string(),
@@ -61,6 +67,11 @@ const actionConfigStructAssist = type({
start_listening: optional(boolean()),
});
const actionConfigStructMoreInfo = type({
action: literal("more-info"),
entity_id: optional(string()),
});
export const actionConfigStructType = object({
action: enums([
"none",
@@ -93,6 +104,12 @@ export const actionConfigStruct = dynamic<any>((value) => {
case "assist": {
return actionConfigStructAssist;
}
case "more-info": {
return actionConfigStructMoreInfo;
}
case "sequence": {
return actionConfigStructSequence;
}
}
}
@@ -204,6 +204,10 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
grid-column: span min(var(--column-size, 1), var(--grid-column-count));
}
.container.edit-mode .card {
min-height: calc((var(--row-height) - var(--row-gap)) / 2);
}
.card.fit-rows {
height: calc(
(var(--row-size, 1) * (var(--row-height) + var(--row-gap))) - var(
+23 -16
View File
@@ -99,31 +99,38 @@ class StateDisplay extends LitElement {
if (content === "name") {
return html`${this.name || stateObj.attributes.friendly_name}`;
}
let relativeDateTime: string | undefined;
// Check last-changed for backwards compatibility
if (content === "last_changed" || content === "last-changed") {
return html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_changed}
capitalize
></ha-relative-time>
`;
relativeDateTime = stateObj.last_changed;
}
// Check last_updated for backwards compatibility
if (content === "last_updated" || content === "last-updated") {
return html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_updated}
capitalize
></ha-relative-time>
`;
relativeDateTime = stateObj.last_updated;
}
if (content === "last_triggered") {
if (
content === "last_triggered" ||
(domain === "calendar" &&
(content === "start_time" || content === "end_time")) ||
(domain === "sun" &&
(content === "next_dawn" ||
content === "next_dusk" ||
content === "next_midnight" ||
content === "next_noon" ||
content === "next_rising" ||
content === "next_setting"))
) {
relativeDateTime = stateObj.attributes[content];
}
if (relativeDateTime) {
return html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.attributes.last_triggered}
.datetime=${relativeDateTime}
capitalize
></ha-relative-time>
`;
+36 -7
View File
@@ -1191,7 +1191,9 @@
"skip": "Skip",
"clear_skipped": "Clear skipped",
"install": "Install",
"create_backup": "Create backup before updating"
"create_backup": "Create backup before updating",
"auto_update_enabled_title": "Can not skip version",
"auto_update_enabled_text": "Automatic updates for this item have been enabled; skipping it is, therefore, unavailable. You can either install this update now or wait for Home Assistant to do it automatically."
},
"updater": {
"title": "Update instructions"
@@ -4039,18 +4041,25 @@
"unknown_automation": "Unknown automation",
"create": "Create automation with {type}",
"create_disable": "Can't create automation with disabled {type}",
"new": {
"title": "Create new automation",
"description": "Start with an empty automation from scratch"
},
"triggers": {
"caption": "Do something when…",
"title": "Use device as trigger",
"description": "When something happens to the device",
"no_triggers": "No triggers",
"unknown_trigger": "Unknown trigger"
},
"conditions": {
"caption": "Only do something if…",
"title": "Use device as condition",
"description": "Only if a condition is met for the device",
"no_conditions": "No conditions",
"unknown_condition": "Unknown condition"
},
"actions": {
"caption": "When something is triggered…",
"title": "Use device as action",
"description": "Do something on the device",
"no_actions": "No actions",
"unknown_action": "Unknown action"
},
@@ -4061,7 +4070,15 @@
"scripts": "scripts",
"no_scripts": "No scripts",
"create": "Create script with {type}",
"create_disable": "Can't create script with disabled {type}"
"create_disable": "Can't create script with disabled {type}",
"new": {
"title": "Create new script",
"description": "Start with an empty script from scratch"
},
"actions": {
"title": "Use device as action",
"description": "Do something on this device."
}
},
"scene": {
"scenes_heading": "Scenes",
@@ -4590,6 +4607,7 @@
"confirm_delete_dataset": "Delete {name} dataset?",
"confirm_delete_dataset_text": "This network will be removed from Home Assistant.",
"no_border_routers": "No border routers found",
"no_preferred_router": "No preferred border router defined",
"border_routers": "{count} border {count, plural,\n one {router}\n other {routers}\n}",
"managed_by_home_assistant": "Managed by Home Assistant",
"operational_dataset": "Operational dataset",
@@ -5734,10 +5752,12 @@
"more-info": "More info",
"toggle": "Toggle",
"navigate": "Navigate",
"sequence": "Sequence",
"assist": "Assist",
"url": "URL",
"none": "Nothing"
}
},
"sequence_actions": "Actions"
},
"condition-editor": {
"explanation": "The card will be shown when ALL conditions below are fulfilled.",
@@ -6881,7 +6901,8 @@
"stop_listening": "Stop listening",
"clear_events": "Clear events",
"alert_event_type": "Event type is a mandatory field",
"notification_event_fired": "Event {type} successfully fired!"
"notification_event_fired": "Event {type} successfully fired!",
"subscribe_failed": "Failed to subscribe to event: {error}"
},
"actions": {
"title": "Actions",
@@ -6966,11 +6987,18 @@
"entity_no_longer_recorded": "This entity is no longer being recorded.",
"no_state": "There is no state available for this entity."
},
"delete_selected": "Delete selected statistics",
"multi_delete": {
"title": "Delete selected statistics",
"info_text": "Do you want to permanently delete the long term statistics {statistic_count, plural,\n one {of this entity}\n other {of {statistic_count} entities}\n} from your database?"
},
"fix_issue": {
"fix": "Fix issue",
"clearing_failed": "Clearing the statistics failed",
"clearing_timeout_title": "Clearing not done yet",
"clearing_timeout_text": "The clearing of the statistics took longer than expected, it might take longer for the issue to disappear.",
"fix_all": "Fix all",
"info": "Info",
"no_support": {
"title": "Fix issue",
"info_text_1": "Fixing this issue is not supported yet."
@@ -7029,6 +7057,7 @@
},
"adjust_sum": "Adjust sum",
"data_table": {
"select_all_issues": "Select all with issues",
"name": "Name",
"statistic_id": "Statistic id",
"statistics_unit": "Statistics unit",
+672 -680
View File
File diff suppressed because it is too large Load Diff