Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e642c80003 | ||
|
|
cc07d51613 | ||
|
|
23a33b10a1 | ||
|
|
67a93013c7 | ||
|
|
1f838d7529 | ||
|
|
ffc0435144 | ||
|
|
5877d69c87 | ||
|
|
99035cea8f | ||
|
|
1b441a7eec | ||
|
|
ad49e9f7b0 | ||
|
|
e32b15ede2 | ||
|
|
a35b4376ea | ||
|
|
619f9f76ee | ||
|
|
f771bc10db | ||
|
|
b8889a1183 | ||
|
|
eb6b45eaed | ||
|
|
31a748ed93 | ||
|
|
0110bdd24a | ||
|
|
365b712976 | ||
|
|
7d97dbe15b | ||
|
|
8bc0ea5a0b | ||
|
|
44948a3474 | ||
|
|
bc51b53b4a |
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20241002.4"
|
||||
version = "20241002.2"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -20,15 +20,6 @@ function findNestedItem(
|
||||
}, obj);
|
||||
}
|
||||
|
||||
function updateNestedItem(obj: any, path: ItemPath): any {
|
||||
const lastKey = path.pop()!;
|
||||
const parent = findNestedItem(obj, path);
|
||||
parent[lastKey] = Array.isArray(parent[lastKey])
|
||||
? [...parent[lastKey]]
|
||||
: [parent[lastKey]];
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function nestedArrayMove<A>(
|
||||
obj: A,
|
||||
oldIndex: number,
|
||||
@@ -36,18 +27,14 @@ export function nestedArrayMove<A>(
|
||||
oldPath?: ItemPath,
|
||||
newPath?: ItemPath
|
||||
): A {
|
||||
let newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
|
||||
|
||||
if (oldPath) {
|
||||
newObj = updateNestedItem(newObj, [...oldPath]);
|
||||
}
|
||||
if (newPath) {
|
||||
newObj = updateNestedItem(newObj, [...newPath]);
|
||||
}
|
||||
|
||||
const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
|
||||
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
|
||||
const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;
|
||||
|
||||
if (!Array.isArray(from) || !Array.isArray(to)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const item = from.splice(oldIndex, 1)[0];
|
||||
to.splice(newIndex, 0, item);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -45,7 +45,7 @@ export class HaControlButton extends LitElement {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
@@ -118,7 +118,6 @@ export class HaPasswordField extends LitElement {
|
||||
.type=${this._unmaskedPassword ? "text" : "password"}
|
||||
.suffix=${html`<div style="width: 24px"></div>`}
|
||||
@input=${this._handleInputChange}
|
||||
@change=${this._reDispatchEvent}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
toggles
|
||||
@@ -157,12 +156,6 @@ export class HaPasswordField extends LitElement {
|
||||
this.value = ev.target.value;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _reDispatchEvent(oldEvent: Event) {
|
||||
const newEvent = new Event(oldEvent.type, oldEvent);
|
||||
this.dispatchEvent(newEvent);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PropertyValues, ReactiveElement } from "lit";
|
||||
import { parseISO } from "date-fns";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { relativeTime } from "../common/datetime/relative_time";
|
||||
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
|
||||
@@ -59,12 +58,7 @@ class HaRelativeTime extends ReactiveElement {
|
||||
if (!this.datetime) {
|
||||
this.innerHTML = this.hass.localize("ui.components.relative_time.never");
|
||||
} else {
|
||||
const date =
|
||||
typeof this.datetime === "string"
|
||||
? parseISO(this.datetime)
|
||||
: this.datetime;
|
||||
|
||||
const relTime = relativeTime(date, this.hass.locale);
|
||||
const relTime = relativeTime(new Date(this.datetime), this.hass.locale);
|
||||
this.innerHTML = this.capitalize
|
||||
? capitalizeFirstLetter(relTime)
|
||||
: relTime;
|
||||
|
||||
@@ -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 = {};
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
@@ -51,7 +51,6 @@ export class HuiPersistentNotificationItem extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.time {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 6px;
|
||||
|
||||
@@ -39,9 +39,6 @@ export const AssistantSetupStyles = [
|
||||
.footer.full-width ha-button {
|
||||
width: 100%;
|
||||
}
|
||||
.footer.centered {
|
||||
justify-content: center;
|
||||
}
|
||||
.footer.side-by-side {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { VoiceAssistantSetupDialogParams } from "./show-voice-assistant-setup-dialog";
|
||||
import "./voice-assistant-setup-step-addons";
|
||||
import "./voice-assistant-setup-step-area";
|
||||
import "./voice-assistant-setup-step-change-wake-word";
|
||||
import "./voice-assistant-setup-step-check";
|
||||
@@ -33,6 +34,7 @@ export const enum STEP {
|
||||
PIPELINE,
|
||||
SUCCESS,
|
||||
CLOUD,
|
||||
ADDONS,
|
||||
CHANGE_WAKEWORD,
|
||||
}
|
||||
|
||||
@@ -208,18 +210,22 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
? html`<ha-voice-assistant-setup-step-cloud
|
||||
.hass=${this.hass}
|
||||
></ha-voice-assistant-setup-step-cloud>`
|
||||
: this._step === STEP.SUCCESS
|
||||
? html`<ha-voice-assistant-setup-step-success
|
||||
: this._step === STEP.ADDONS
|
||||
? html`<ha-voice-assistant-setup-step-addons
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this
|
||||
._assistConfiguration}
|
||||
.assistEntityId=${this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-success>`
|
||||
: nothing}
|
||||
></ha-voice-assistant-setup-step-addons>`
|
||||
: this._step === STEP.SUCCESS
|
||||
? html`<ha-voice-assistant-setup-step-success
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this
|
||||
._assistConfiguration}
|
||||
.assistEntityId=${this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-success>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
import { STEP } from "./voice-assistant-setup-dialog";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
|
||||
@customElement("ha-voice-assistant-setup-step-addons")
|
||||
export class HaVoiceAssistantSetupStepAddons extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _showFirst = false;
|
||||
|
||||
@state() private _showSecond = false;
|
||||
|
||||
@state() private _showThird = false;
|
||||
|
||||
@state() private _showFourth = false;
|
||||
|
||||
protected override firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
setTimeout(() => {
|
||||
this._showFirst = true;
|
||||
}, 200);
|
||||
setTimeout(() => {
|
||||
this._showSecond = true;
|
||||
}, 600);
|
||||
setTimeout(() => {
|
||||
this._showThird = true;
|
||||
}, 3000);
|
||||
setTimeout(() => {
|
||||
this._showFourth = true;
|
||||
}, 8000);
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
return html`<div class="content">
|
||||
<h1>Local</h1>
|
||||
<p class="secondary">
|
||||
Are you sure you want to use the local voice assistant? It requires a
|
||||
powerful device to run. If you device is not powerful enough, Home
|
||||
Assistant cloud might be a better option.
|
||||
</p>
|
||||
<h3>Raspberry Pi 4</h3>
|
||||
<div class="messages-container rpi">
|
||||
<div class="message user ${this._showThird ? "show" : ""}">
|
||||
${!this._showThird ? "…" : "Turn on the lights in the bedroom"}
|
||||
</div>
|
||||
${this._showThird
|
||||
? html`<div class="timing user">3 seconds</div>`
|
||||
: nothing}
|
||||
${this._showThird
|
||||
? html`<div class="message hass ${this._showFourth ? "show" : ""}">
|
||||
${!this._showFourth ? "…" : "Turned on the lights"}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._showFourth
|
||||
? html`<div class="timing hass">5 seconds</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<h3>Home Assistant Cloud</h3>
|
||||
<div class="messages-container cloud">
|
||||
<div class="message user ${this._showFirst ? "show" : ""}">
|
||||
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
|
||||
</div>
|
||||
${this._showFirst
|
||||
? html`<div class="timing user">0.2 seconds</div>`
|
||||
: nothing}
|
||||
${this._showFirst
|
||||
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
|
||||
${!this._showSecond ? "…" : "Turned on the lights"}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._showSecond
|
||||
? html`<div class="timing hass">0.4 seconds</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer side-by-side">
|
||||
<ha-button @click=${this._goToCloud}
|
||||
>Try Home Assistant Cloud</ha-button
|
||||
>
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/voice_control/voice_remote_local_assistant/"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer noopenner"
|
||||
>
|
||||
<ha-button @click=${this._skip} unelevated>Learn more</ha-button>
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _goToCloud() {
|
||||
fireEvent(this, "next-step", { step: STEP.CLOUD });
|
||||
}
|
||||
|
||||
private _skip() {
|
||||
fireEvent(this, "next-step", { step: STEP.SUCCESS });
|
||||
}
|
||||
|
||||
static styles = [
|
||||
AssistantSetupStyles,
|
||||
css`
|
||||
.messages-container {
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
height: 195px;
|
||||
background: var(--input-fill-color);
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--divider-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.message {
|
||||
white-space: nowrap;
|
||||
font-size: 18px;
|
||||
clear: both;
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
border-radius: 15px;
|
||||
height: 36px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 30px;
|
||||
}
|
||||
.rpi .message {
|
||||
transition: width 1s;
|
||||
}
|
||||
.cloud .message {
|
||||
transition: width 0.5s;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: initial;
|
||||
align-self: self-end;
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0px;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-primary-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.timing.user {
|
||||
align-self: self-end;
|
||||
}
|
||||
|
||||
.message.user.show {
|
||||
width: 295px;
|
||||
}
|
||||
|
||||
.message.hass {
|
||||
margin-right: 24px;
|
||||
margin-inline-end: 24px;
|
||||
margin-inline-start: initial;
|
||||
align-self: self-start;
|
||||
border-bottom-left-radius: 0px;
|
||||
background-color: var(--secondary-background-color);
|
||||
color: var(--primary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.timing.hass {
|
||||
align-self: self-start;
|
||||
}
|
||||
|
||||
.message.hass.show {
|
||||
width: 184px;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-voice-assistant-setup-step-addons": HaVoiceAssistantSetupStepAddons;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export class HaVoiceAssistantSetupStepArea extends LitElement {
|
||||
const device = this.hass.devices[this.deviceId];
|
||||
|
||||
return html`<div class="content">
|
||||
<img src="/static/images/voice-assistant/area.gif" />
|
||||
<img src="/static/icons/casita/loving.png" />
|
||||
<h1>Select area</h1>
|
||||
<p class="secondary">
|
||||
When you voice assistant knows where it is, it can better control the
|
||||
|
||||
@@ -10,7 +10,6 @@ import { STEP } from "./voice-assistant-setup-dialog";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
import "../../components/ha-md-list";
|
||||
import "../../components/ha-md-list-item";
|
||||
import { formatLanguageCode } from "../../common/language/format_language";
|
||||
|
||||
@customElement("ha-voice-assistant-setup-step-change-wake-word")
|
||||
export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
|
||||
@@ -23,12 +22,11 @@ export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement {
|
||||
|
||||
protected override render() {
|
||||
return html`<div class="padding content">
|
||||
<img src="/static/images/voice-assistant/change-wake-word.gif" />
|
||||
<img src="/static/icons/casita/smiling.png" />
|
||||
<h1>Change wake word</h1>
|
||||
<p class="secondary">
|
||||
Some wake words are better for
|
||||
${formatLanguageCode(this.hass.locale.language, this.hass.locale)} and
|
||||
voice than others. Please try them out.
|
||||
Some wake words are better for [your language] and voice than others.
|
||||
Please try them out.
|
||||
</p>
|
||||
</div>
|
||||
<ha-md-list>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { testAssistSatelliteConnection } from "../../data/assist_satellite";
|
||||
@@ -13,8 +13,6 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
|
||||
|
||||
@state() private _status?: "success" | "timeout";
|
||||
|
||||
@state() private _showLoader = false;
|
||||
|
||||
protected override willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (!this.hasUpdated) {
|
||||
@@ -32,48 +30,39 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
|
||||
|
||||
protected override render() {
|
||||
return html`<div class="content">
|
||||
${this._status === "timeout"
|
||||
? html`<img src="/static/images/voice-assistant/error.gif" />
|
||||
<h1>The voice assistant is unable to connect to Home Assistant</h1>
|
||||
<p class="secondary">
|
||||
To play audio, the voice assistant device has to connect to Home
|
||||
Assistant to fetch the files. Our test shows that the device is
|
||||
unable to reach the Home Assistant server.
|
||||
</p>
|
||||
<div class="footer">
|
||||
<a
|
||||
href="https://www.home-assistant.io/docs/configuration/remote/#adding-a-remote-url-to-home-assistant"
|
||||
><ha-button>Help me</ha-button></a
|
||||
>
|
||||
<ha-button @click=${this._testConnection}>Retry</ha-button>
|
||||
</div>`
|
||||
: html`<img src="/static/images/voice-assistant/hi.gif" />
|
||||
${this._status === "success"
|
||||
? html`<img src="/static/icons/casita/smiling.png" />
|
||||
<h1>Hi</h1>
|
||||
<p class="secondary">
|
||||
Over the next couple steps we're going to personalize your voice
|
||||
assistant.
|
||||
</p>
|
||||
|
||||
${this._showLoader
|
||||
? html`<ha-circular-progress
|
||||
indeterminate
|
||||
></ha-circular-progress>`
|
||||
: nothing} `}
|
||||
With a couple of steps we are going to setup your voice assistant.
|
||||
</p>`
|
||||
: this._status === "timeout"
|
||||
? html`<img src="/static/icons/casita/sad.png" />
|
||||
<h1>Voice assistant can not connect to Home Assistant</h1>
|
||||
<p class="secondary">
|
||||
A good explanation what is happening and what action you should
|
||||
take.
|
||||
</p>
|
||||
<div class="footer">
|
||||
<a href="#"><ha-button>Help me</ha-button></a>
|
||||
<ha-button @click=${this._testConnection}>Retry</ha-button>
|
||||
</div>`
|
||||
: html`<img src="/static/icons/casita/loading.png" />
|
||||
<h1>Checking...</h1>
|
||||
<p class="secondary">
|
||||
We are checking if the device can reach your Home Assistant
|
||||
instance.
|
||||
</p>
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _testConnection() {
|
||||
this._status = undefined;
|
||||
this._showLoader = false;
|
||||
const timeout = setTimeout(() => {
|
||||
this._showLoader = true;
|
||||
}, 3000);
|
||||
const result = await testAssistSatelliteConnection(
|
||||
this.hass,
|
||||
this.assistEntityId!
|
||||
);
|
||||
clearTimeout(timeout);
|
||||
this._showLoader = false;
|
||||
this._status = result.status;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { mdiEarth, mdiMicrophoneMessage, mdiOpenInNew } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
|
||||
@customElement("ha-voice-assistant-setup-step-cloud")
|
||||
@@ -12,92 +10,22 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
|
||||
|
||||
protected override render() {
|
||||
return html`<div class="content">
|
||||
<img
|
||||
src=${`/static/images/logo_nabu_casa${this.hass.themes?.darkMode ? "_dark" : ""}.png`}
|
||||
alt="Nabu Casa logo"
|
||||
/>
|
||||
<h1>The power of Home Assistant Cloud</h1>
|
||||
<div class="features">
|
||||
<div class="feature speech">
|
||||
<div class="logos">
|
||||
<div class="round-icon">
|
||||
<ha-svg-icon .path=${mdiMicrophoneMessage}></ha-svg-icon>
|
||||
</div>
|
||||
</div>
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.cloud.features.speech.title"
|
||||
)}
|
||||
<span class="no-wrap"></span>
|
||||
</h2>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.cloud.features.speech.text"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature access">
|
||||
<div class="logos">
|
||||
<div class="round-icon">
|
||||
<ha-svg-icon .path=${mdiEarth}></ha-svg-icon>
|
||||
</div>
|
||||
</div>
|
||||
<h2>
|
||||
Remote access
|
||||
<span class="no-wrap"></span>
|
||||
</h2>
|
||||
<p>
|
||||
Secure remote access to your system while supporting the
|
||||
development of Home Assistant.
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="logos">
|
||||
<img
|
||||
alt="Google Assistant"
|
||||
src=${brandsUrl({
|
||||
domain: "google_assistant",
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<img
|
||||
alt="Amazon Alexa"
|
||||
src=${brandsUrl({
|
||||
domain: "alexa",
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.cloud.features.assistants.title"
|
||||
)}
|
||||
</h2>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.assistants.cloud.features.assistants.text"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<img src="/static/images/logo_nabu_casa.png" />
|
||||
<h1>Supercharge your assistant with Home Assistant Cloud</h1>
|
||||
<p class="secondary">
|
||||
Speed up and take the load off your system by running your
|
||||
text-to-speech and speech-to-text in our private and secure cloud.
|
||||
Cloud also includes secure remote access to your system while
|
||||
supporting the development of Home Assistant.
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer side-by-side">
|
||||
<a
|
||||
href="https://www.nabucasa.com"
|
||||
target="_blank"
|
||||
rel="noreferrer noopenner"
|
||||
><ha-button>Learn more</ha-button></a
|
||||
>
|
||||
<ha-button>
|
||||
<ha-svg-icon .path=${mdiOpenInNew} slot="icon"></ha-svg-icon>
|
||||
nabucasa.com
|
||||
</ha-button>
|
||||
</a>
|
||||
<a href="/config/cloud/register" @click=${this._close}
|
||||
><ha-button unelevated>Try 1 month for free</ha-button></a
|
||||
>
|
||||
@@ -108,58 +36,7 @@ export class HaVoiceAssistantSetupStepCloud extends LitElement {
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
|
||||
static styles = [
|
||||
AssistantSetupStyles,
|
||||
css`
|
||||
.features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
.feature {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.feature .logos {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.feature .logos > * {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.round-icon {
|
||||
border-radius: 50%;
|
||||
color: #6e41ab;
|
||||
background-color: #e8dcf7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
}
|
||||
.access .round-icon {
|
||||
color: #00aef8;
|
||||
background-color: #cceffe;
|
||||
}
|
||||
.feature h2 {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.feature p {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
static styles = AssistantSetupStyles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -32,10 +32,6 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
|
||||
@state() private _showSecond = false;
|
||||
|
||||
@state() private _showThird = false;
|
||||
|
||||
@state() private _showFourth = false;
|
||||
|
||||
protected override willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
@@ -48,83 +44,63 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
super.firstUpdated(changedProperties);
|
||||
setTimeout(() => {
|
||||
this._showFirst = true;
|
||||
}, 200);
|
||||
}, 1);
|
||||
setTimeout(() => {
|
||||
this._showSecond = true;
|
||||
}, 600);
|
||||
setTimeout(() => {
|
||||
this._showThird = true;
|
||||
}, 3000);
|
||||
setTimeout(() => {
|
||||
this._showFourth = true;
|
||||
}, 8000);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
return html`<div class="content">
|
||||
<h1>What hardware do you want to use?</h1>
|
||||
<p class="secondary">
|
||||
How quickly your assistant responds depends on the power of the
|
||||
hardware.
|
||||
</p>
|
||||
<div class="container">
|
||||
<div class="messages-container cloud">
|
||||
return html`<div class="padding content">
|
||||
<div class="messages-container">
|
||||
<div class="message user ${this._showFirst ? "show" : ""}">
|
||||
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
|
||||
</div>
|
||||
${this._showFirst
|
||||
? html`<div class="timing user">0.2 seconds</div>`
|
||||
: nothing}
|
||||
${this._showFirst
|
||||
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
|
||||
${!this._showSecond ? "…" : "Turned on the lights"}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._showSecond
|
||||
? html`<div class="timing hass">0.4 seconds</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<h2>Home Assistant Cloud</h2>
|
||||
<p>Ideal if you don't have a powerful system at home.</p>
|
||||
<ha-button @click=${this._setupCloud}>Learn more</ha-button>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="messages-container rpi">
|
||||
<div class="message user ${this._showThird ? "show" : ""}">
|
||||
${!this._showThird ? "…" : "Turn on the lights in the bedroom"}
|
||||
</div>
|
||||
${this._showThird
|
||||
? html`<div class="timing user">3 seconds</div>`
|
||||
: nothing}
|
||||
${this._showThird
|
||||
? html`<div class="message hass ${this._showFourth ? "show" : ""}">
|
||||
${!this._showFourth ? "…" : "Turned on the lights"}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._showFourth
|
||||
? html`<div class="timing hass">5 seconds</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<h2>Do-it-yourself</h2>
|
||||
<p>
|
||||
Install add-ons or containers to run it on your own system. Powerful
|
||||
hardware is needed for fast responses.
|
||||
<h1>Select system</h1>
|
||||
<p class="secondary">
|
||||
How quickly your voice assistant responds depends on the power of your
|
||||
system.
|
||||
</p>
|
||||
<a
|
||||
</div>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item interactive type="button" @click=${this._setupCloud}>
|
||||
Home Assistant Cloud
|
||||
<span slot="supporting-text"
|
||||
>Ideal if you don't have a powerful system at home</span
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item interactive type="button" @click=${this._thisSystem}>
|
||||
On this system
|
||||
<span slot="supporting-text"
|
||||
>Local setup with the Whisper and Piper add-ons</span
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item
|
||||
interactive
|
||||
type="link"
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/voice_control/voice_remote_local_assistant/"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer noopenner"
|
||||
target="_blank"
|
||||
@click=${this._skip}
|
||||
>
|
||||
<ha-button @click=${this._skip}>
|
||||
<ha-svg-icon .path=${mdiOpenInNew} slot="icon"></ha-svg-icon>
|
||||
Learn more</ha-button
|
||||
Use external system
|
||||
<span slot="supporting-text"
|
||||
>Learn more about how to host it on another system</span
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>`;
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>`;
|
||||
}
|
||||
|
||||
private async _checkCloud() {
|
||||
@@ -241,6 +217,10 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
this._nextStep(STEP.CLOUD);
|
||||
}
|
||||
|
||||
private async _thisSystem() {
|
||||
this._nextStep(STEP.ADDONS);
|
||||
}
|
||||
|
||||
private _skip() {
|
||||
this._nextStep(STEP.SUCCESS);
|
||||
}
|
||||
@@ -252,22 +232,21 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
static styles = [
|
||||
AssistantSetupStyles,
|
||||
css`
|
||||
.container {
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--divider-color);
|
||||
overflow: hidden;
|
||||
padding-bottom: 16px;
|
||||
:host {
|
||||
padding: 0;
|
||||
}
|
||||
.container:last-child {
|
||||
margin-top: 16px;
|
||||
.padding {
|
||||
padding: 24px;
|
||||
}
|
||||
ha-md-list {
|
||||
width: 100%;
|
||||
text-align: initial;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
height: 195px;
|
||||
background: var(--input-fill-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 152px;
|
||||
}
|
||||
.message {
|
||||
white-space: nowrap;
|
||||
@@ -280,29 +259,21 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 30px;
|
||||
}
|
||||
.rpi .message {
|
||||
transition: width 1s;
|
||||
}
|
||||
.cloud .message {
|
||||
transition: width 0.5s;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: initial;
|
||||
align-self: self-end;
|
||||
float: var(--float-end);
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0px;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-primary-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.timing.user {
|
||||
align-self: self-end;
|
||||
}
|
||||
|
||||
.message.user.show {
|
||||
width: 295px;
|
||||
@@ -312,15 +283,12 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
|
||||
margin-right: 24px;
|
||||
margin-inline-end: 24px;
|
||||
margin-inline-start: initial;
|
||||
align-self: self-start;
|
||||
float: var(--float-start);
|
||||
border-bottom-left-radius: 0px;
|
||||
background-color: var(--secondary-background-color);
|
||||
color: var(--primary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.timing.hass {
|
||||
align-self: self-start;
|
||||
}
|
||||
|
||||
.message.hass.show {
|
||||
width: 184px;
|
||||
|
||||
@@ -67,11 +67,11 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
: undefined;
|
||||
|
||||
return html`<div class="content">
|
||||
<img src="/static/images/voice-assistant/heart.gif" />
|
||||
<h1>Ready to Assist!</h1>
|
||||
<img src="/static/icons/casita/loving.png" />
|
||||
<h1>Ready to assist!</h1>
|
||||
<p class="secondary">
|
||||
Make any final customizations here. You can always change these in the
|
||||
Voice Assistants section of the settings page.
|
||||
Your device is all ready to go! If you want to tweak some more
|
||||
settings, you can change that below.
|
||||
</p>
|
||||
<div class="rows">
|
||||
${this.assistConfiguration &&
|
||||
|
||||
@@ -2,13 +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 { ON, UNAVAILABLE } from "../../data/entity";
|
||||
import {
|
||||
updateCanInstall,
|
||||
UpdateEntity,
|
||||
updateIsInstalling,
|
||||
updateUsesProgress,
|
||||
} from "../../data/update";
|
||||
import { OFF, ON, UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
|
||||
@@ -57,19 +51,17 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this.updateEntityId] as
|
||||
| UpdateEntity
|
||||
| undefined;
|
||||
const stateObj = this.hass.states[this.updateEntityId];
|
||||
|
||||
const progressIsNumeric = stateObj && updateUsesProgress(stateObj);
|
||||
const progressIsNumeric =
|
||||
typeof stateObj?.attributes.in_progress === "number";
|
||||
|
||||
return html`<div class="content">
|
||||
<img src="/static/images/voice-assistant/update.gif" />
|
||||
<img src="/static/icons/casita/loading.png" />
|
||||
<h1>
|
||||
${stateObj &&
|
||||
(stateObj.state === "unavailable" || updateIsInstalling(stateObj))
|
||||
? "Updating your voice assistant"
|
||||
: "Checking for updates"}
|
||||
${stateObj.state === OFF || stateObj.state === UNKNOWN
|
||||
? "Checking for updates"
|
||||
: "Updating your voice assistant"}
|
||||
</h1>
|
||||
<p class="secondary">
|
||||
We are making sure you have the latest and greatest version of your
|
||||
@@ -77,12 +69,12 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
</p>
|
||||
<ha-circular-progress
|
||||
.value=${progressIsNumeric
|
||||
? (stateObj.attributes.in_progress as number) / 100
|
||||
? stateObj.attributes.in_progress / 100
|
||||
: undefined}
|
||||
.indeterminate=${!progressIsNumeric}
|
||||
></ha-circular-progress>
|
||||
<p>
|
||||
${stateObj?.state === UNAVAILABLE
|
||||
${stateObj.state === "unavailable"
|
||||
? "Restarting voice assistant"
|
||||
: progressIsNumeric
|
||||
? `Installing ${stateObj.attributes.in_progress}%`
|
||||
@@ -96,14 +88,8 @@ export class HaVoiceAssistantSetupStepUpdate extends LitElement {
|
||||
if (!this.updateEntityId) {
|
||||
return;
|
||||
}
|
||||
const updateEntity = this.hass.states[this.updateEntityId] as
|
||||
| UpdateEntity
|
||||
| undefined;
|
||||
if (
|
||||
updateEntity &&
|
||||
this.hass.states[updateEntity.entity_id].state === ON &&
|
||||
updateCanInstall(updateEntity)
|
||||
) {
|
||||
const updateEntity = this.hass.states[this.updateEntityId];
|
||||
if (updateEntity && this.hass.states[updateEntity.entity_id].state === ON) {
|
||||
this._updated = true;
|
||||
await this.hass.callService(
|
||||
"update",
|
||||
|
||||
@@ -65,14 +65,14 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
||||
return html`<div class="content">
|
||||
${!this._detected
|
||||
? html`
|
||||
<img src="/static/images/voice-assistant/sleep.gif" />
|
||||
<img src="/static/icons/casita/sleeping.png" />
|
||||
<h1>
|
||||
Say “${this._activeWakeWord(this.assistConfiguration)}” to wake the
|
||||
device up
|
||||
</h1>
|
||||
<p class="secondary">Setup will continue once the device is awake.</p>
|
||||
</div>`
|
||||
: html`<img src="/static/images/voice-assistant/ok-nabu.gif" />
|
||||
: html`<img src="/static/icons/casita/normal.png" />
|
||||
<h1>
|
||||
Say “${this._activeWakeWord(this.assistConfiguration)}” again
|
||||
</h1>
|
||||
@@ -80,7 +80,7 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
||||
To make sure the wake word works for you.
|
||||
</p>`}
|
||||
</div>
|
||||
<div class="footer centered">
|
||||
<div class="footer full-width">
|
||||
<ha-button @click=${this._changeWakeWord}>Change wake word</ha-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -774,29 +774,27 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
: ""}
|
||||
</div>
|
||||
${item.disabled_by === "user"
|
||||
? html`<ha-button unelevated slot="end" @click=${this._handleEnable}>
|
||||
? html`<mwc-button unelevated slot="end" @click=${this._handleEnable}>
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
</ha-button>`
|
||||
</mwc-button>`
|
||||
: configPanel &&
|
||||
(item.domain !== "matter" ||
|
||||
isDevVersion(this.hass.config.version)) &&
|
||||
!stateText
|
||||
(item.domain !== "matter" || isDevVersion(this.hass.config.version))
|
||||
? html`<a
|
||||
slot="end"
|
||||
href=${`/${configPanel}?config_entry=${item.entry_id}`}
|
||||
><ha-button>
|
||||
><mwc-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
</ha-button></a
|
||||
</mwc-button></a
|
||||
>`
|
||||
: item.supports_options
|
||||
: item.supports_options && !stateText
|
||||
? html`
|
||||
<ha-button slot="end" @click=${this._showOptions}>
|
||||
<mwc-button slot="end" @click=${this._showOptions}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
<ha-md-button-menu positioning="popover" slot="end">
|
||||
@@ -805,6 +803,14 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
${item.supports_options && stateText
|
||||
? html`<ha-md-menu-item @click=${this._showOptions}>
|
||||
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
</ha-md-menu-item>`
|
||||
: ""}
|
||||
${item.disabled_by && devices.length
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
|
||||
@@ -482,9 +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
|
||||
? Number(ev.detail.index)
|
||||
: Number(ev.detail.index) + 1;
|
||||
const index = Number(ev.detail.index);
|
||||
switch (index) {
|
||||
case 0:
|
||||
this._setPreferredBorderAgent(network.dataset!, router);
|
||||
|
||||
@@ -83,6 +83,8 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
@state() private _config?: ScriptConfig;
|
||||
|
||||
@state() private _idError = false;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _errors?: string;
|
||||
@@ -412,18 +414,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
this._loadConfig();
|
||||
}
|
||||
|
||||
if (
|
||||
(changedProps.has("scriptId") || changedProps.has("entityRegistry")) &&
|
||||
this.scriptId &&
|
||||
this.entityRegistry
|
||||
) {
|
||||
// find entity for when script entity id changed
|
||||
const entity = this.entityRegistry.find(
|
||||
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
|
||||
);
|
||||
this._entityId = entity?.entity_id;
|
||||
}
|
||||
|
||||
if (changedProps.has("scriptId") && !this.scriptId && this.hass) {
|
||||
const initData = getScriptEditorInitData();
|
||||
this._dirty = !!initData;
|
||||
@@ -458,6 +448,15 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _setEntityId(id?: string) {
|
||||
this._entityId = id;
|
||||
if (this.hass.states[`script.${this._entityId}`]) {
|
||||
this._idError = true;
|
||||
} else {
|
||||
this._idError = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _checkValidation() {
|
||||
this._validationErrors = undefined;
|
||||
if (!this._entityId || !this._config) {
|
||||
@@ -767,12 +766,28 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _saveScript(): Promise<void> {
|
||||
if (this._idError) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.script.editor.id_already_exists_save_error"
|
||||
),
|
||||
dismissable: false,
|
||||
duration: -1,
|
||||
action: {
|
||||
action: () => {},
|
||||
text: this.hass.localize("ui.dialogs.generic.ok"),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.scriptId) {
|
||||
const saved = await this._promptScriptAlias();
|
||||
if (!saved) {
|
||||
return;
|
||||
}
|
||||
this._entityId = this._computeEntityIdFromAlias(this._config!.alias);
|
||||
const entityId = this._computeEntityIdFromAlias(this._config!.alias);
|
||||
this._setEntityId(entityId);
|
||||
}
|
||||
const id = this.scriptId || this._entityId || Date.now();
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ import { showConfirmationDialog } from "../../lovelace/custom-card-helpers";
|
||||
import { fixStatisticsIssue } from "./fix-statistics";
|
||||
import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
|
||||
|
||||
const FIX_ISSUES_ORDER: Record<StatisticsValidationResult["type"], number> = {
|
||||
const FIX_ISSUES_ORDER = {
|
||||
no_state: 0,
|
||||
entity_no_longer_recorded: 1,
|
||||
entity_not_recorded: 1,
|
||||
@@ -57,10 +57,10 @@ const FIX_ISSUES_ORDER: Record<StatisticsValidationResult["type"], number> = {
|
||||
units_changed: 3,
|
||||
};
|
||||
|
||||
const FIXABLE_ISSUES: StatisticsValidationResult["type"][] = [
|
||||
const FIXABLE_ISSUES = [
|
||||
"no_state",
|
||||
"entity_no_longer_recorded",
|
||||
"state_class_removed",
|
||||
"unsupported_state_class",
|
||||
"units_changed",
|
||||
];
|
||||
|
||||
|
||||
@@ -187,6 +187,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
.content p {
|
||||
margin: 0;
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -3684,6 +3684,9 @@
|
||||
"editor": {
|
||||
"alias": "Name",
|
||||
"icon": "Icon",
|
||||
"id": "Entity ID",
|
||||
"id_already_exists_save_error": "You can't save this script because the ID is not unique, pick another ID or leave it blank to automatically generate one.",
|
||||
"id_already_exists": "This ID already exists",
|
||||
"introduction": "Use scripts to run a sequence of actions.",
|
||||
"show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]",
|
||||
"show_info": "[%key:ui::panel::config::automation::editor::show_info%]",
|
||||
@@ -5749,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.",
|
||||
|
||||