mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
20230405.0 (#16067)
This commit is contained in:
commit
d41cf17932
@ -92,11 +92,7 @@ export class HassioAddonStore extends LitElement {
|
||||
.route=${this.route}
|
||||
.header=${this.supervisor.localize("panel.store")}
|
||||
>
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
slot="toolbar-icon"
|
||||
@action=${this._handleAction}
|
||||
>
|
||||
<ha-button-menu slot="toolbar-icon" @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
|
@ -168,7 +168,7 @@ class HassioAddonConfig extends LitElement {
|
||||
${this.supervisor.localize("addon.configuration.options.header")}
|
||||
</h2>
|
||||
<div class="card-menu">
|
||||
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
|
||||
<ha-button-menu @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
|
@ -195,11 +195,7 @@ export class HassioBackups extends LitElement {
|
||||
: "/config"}
|
||||
supervisor
|
||||
>
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
slot="toolbar-icon"
|
||||
@action=${this._handleAction}
|
||||
>
|
||||
<ha-button-menu slot="toolbar-icon" @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor?.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
|
@ -184,7 +184,7 @@ class HassioHostInfo extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-button-menu corner="BOTTOM_START">
|
||||
<ha-button-menu>
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
|
@ -96,8 +96,8 @@
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.8",
|
||||
"@webcomponents/webcomponentsjs": "2.7.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"chart.js": "3.3.2",
|
||||
"comlink": "4.4.1",
|
||||
@ -245,7 +245,7 @@
|
||||
"vinyl-source-stream": "2.0.0",
|
||||
"webpack": "=5.72.1",
|
||||
"webpack-cli": "5.0.1",
|
||||
"webpack-dev-server": "4.13.1",
|
||||
"webpack-dev-server": "4.13.2",
|
||||
"webpack-manifest-plugin": "5.0.0",
|
||||
"webpackbar": "5.0.2",
|
||||
"workbox-build": "6.5.4"
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20230403.0"
|
||||
version = "20230405.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@ -302,7 +302,7 @@ export default class HaChartBase extends LitElement {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
position: var(--chart-base-position, relative);
|
||||
}
|
||||
.chartContainer {
|
||||
overflow: hidden;
|
||||
|
@ -10,7 +10,7 @@ import type { HaIconButton } from "./ha-icon-button";
|
||||
export class HaButtonMenu extends LitElement {
|
||||
protected readonly [FOCUS_TARGET];
|
||||
|
||||
@property() public corner: Corner = "TOP_START";
|
||||
@property() public corner: Corner = "BOTTOM_START";
|
||||
|
||||
@property() public menuCorner: MenuCorner = "START";
|
||||
|
||||
|
@ -35,7 +35,7 @@ interface FilterValue {
|
||||
export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public corner: Corner = "TOP_START";
|
||||
@property() public corner: Corner = "BOTTOM_START";
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
|
@ -82,7 +82,6 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
<ha-button-menu
|
||||
.disabled=${this.disabled}
|
||||
fixed
|
||||
corner="BOTTOM_START"
|
||||
@opened=${this._handleOpen}
|
||||
@closed=${this._handleClose}
|
||||
multi
|
||||
|
@ -38,7 +38,6 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
@click=${this._handleIconOverflowMenuOpened}
|
||||
@closed=${this._handleIconOverflowMenuClosed}
|
||||
class="ha-icon-overflow-menu-overflow"
|
||||
corner="BOTTOM_START"
|
||||
absolute
|
||||
>
|
||||
<ha-icon-button
|
||||
|
@ -70,11 +70,7 @@ class HaQrScanner extends LitElement {
|
||||
? html`<video></video>
|
||||
<div id="canvas-container">
|
||||
${this._cameras && this._cameras.length > 1
|
||||
? html`<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
fixed
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
? html`<ha-button-menu fixed @closed=${stopPropagation}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.localize(
|
||||
|
@ -283,7 +283,6 @@ export class HaTargetPicker extends LitElement {
|
||||
return html`<mwc-menu-surface
|
||||
open
|
||||
.anchor=${this._addContainer}
|
||||
.corner=${"BOTTOM_START"}
|
||||
@closed=${this._onClosed}
|
||||
@opened=${this._onOpened}
|
||||
@opened-changed=${this._openedChanged}
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { stateActive } from "../common/entity/state_active";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import { UNAVAILABLE } from "./entity";
|
||||
@ -114,10 +115,12 @@ export function computeCoverPositionStateDisplay(
|
||||
locale: FrontendLocaleData,
|
||||
position?: number
|
||||
) {
|
||||
const currentPosition =
|
||||
position ??
|
||||
stateObj.attributes.current_position ??
|
||||
stateObj.attributes.current_tilt_position;
|
||||
const statePosition = stateActive(stateObj)
|
||||
? stateObj.attributes.current_position ??
|
||||
stateObj.attributes.current_tilt_position
|
||||
: undefined;
|
||||
|
||||
const currentPosition = position ?? statePosition;
|
||||
|
||||
return currentPosition && currentPosition !== 100
|
||||
? `${Math.round(currentPosition)}${blankBeforePercent(locale)}%`
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { stateActive } from "../common/entity/state_active";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import { FrontendLocaleData } from "./translation";
|
||||
|
||||
@ -69,7 +70,7 @@ export function fanSpeedToPercentage(
|
||||
if (speedValue === -1) {
|
||||
return 0;
|
||||
}
|
||||
return Math.round(speedValue * step);
|
||||
return Math.floor(speedValue * step);
|
||||
}
|
||||
|
||||
export function computeFanSpeedCount(stateObj: FanEntity): number {
|
||||
@ -99,9 +100,12 @@ export function computeFanSpeedStateDisplay(
|
||||
locale: FrontendLocaleData,
|
||||
speed?: number
|
||||
) {
|
||||
const currentSpeed = speed ?? stateObj.attributes.percentage;
|
||||
const percentage = stateActive(stateObj)
|
||||
? stateObj.attributes.percentage
|
||||
: undefined;
|
||||
const currentSpeed = speed ?? percentage;
|
||||
|
||||
return currentSpeed
|
||||
? `${Math.round(currentSpeed)}${blankBeforePercent(locale)}%`
|
||||
? `${Math.floor(currentSpeed)}${blankBeforePercent(locale)}%`
|
||||
: "";
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ type PipelineRunEvent =
|
||||
| PipelineTTSStartEvent
|
||||
| PipelineTTSEndEvent;
|
||||
|
||||
interface PipelineRunOptions {
|
||||
export interface PipelineRunOptions {
|
||||
start_stage: "stt" | "intent" | "tts";
|
||||
end_stage: "stt" | "intent" | "tts";
|
||||
language?: string;
|
||||
@ -99,13 +99,15 @@ export interface PipelineRun {
|
||||
stage: "ready" | "stt" | "intent" | "tts" | "done" | "error";
|
||||
run: PipelineRunStartEvent["data"];
|
||||
error?: PipelineErrorEvent["data"];
|
||||
stt?: PipelineSTTStartEvent["data"] & Partial<PipelineSTTEndEvent["data"]>;
|
||||
stt?: PipelineSTTStartEvent["data"] &
|
||||
Partial<PipelineSTTEndEvent["data"]> & { done: boolean };
|
||||
intent?: PipelineIntentStartEvent["data"] &
|
||||
Partial<PipelineIntentEndEvent["data"]>;
|
||||
tts?: PipelineTTSStartEvent["data"] & Partial<PipelineTTSEndEvent["data"]>;
|
||||
Partial<PipelineIntentEndEvent["data"]> & { done: boolean };
|
||||
tts?: PipelineTTSStartEvent["data"] &
|
||||
Partial<PipelineTTSEndEvent["data"]> & { done: boolean };
|
||||
}
|
||||
|
||||
export const runPipelineFromText = (
|
||||
export const runVoiceAssistantPipeline = (
|
||||
hass: HomeAssistant,
|
||||
callback: (event: PipelineRun) => void,
|
||||
options: PipelineRunOptions
|
||||
@ -139,17 +141,38 @@ export const runPipelineFromText = (
|
||||
}
|
||||
|
||||
if (updateEvent.type === "stt-start") {
|
||||
run = { ...run, stage: "stt", stt: updateEvent.data };
|
||||
run = {
|
||||
...run,
|
||||
stage: "stt",
|
||||
stt: { ...updateEvent.data, done: false },
|
||||
};
|
||||
} else if (updateEvent.type === "stt-end") {
|
||||
run = { ...run, stt: { ...run.stt!, ...updateEvent.data } };
|
||||
run = {
|
||||
...run,
|
||||
stt: { ...run.stt!, ...updateEvent.data, done: true },
|
||||
};
|
||||
} else if (updateEvent.type === "intent-start") {
|
||||
run = { ...run, stage: "intent", intent: updateEvent.data };
|
||||
run = {
|
||||
...run,
|
||||
stage: "intent",
|
||||
intent: { ...updateEvent.data, done: false },
|
||||
};
|
||||
} else if (updateEvent.type === "intent-end") {
|
||||
run = { ...run, intent: { ...run.intent!, ...updateEvent.data } };
|
||||
run = {
|
||||
...run,
|
||||
intent: { ...run.intent!, ...updateEvent.data, done: true },
|
||||
};
|
||||
} else if (updateEvent.type === "tts-start") {
|
||||
run = { ...run, stage: "tts", tts: updateEvent.data };
|
||||
run = {
|
||||
...run,
|
||||
stage: "tts",
|
||||
tts: { ...updateEvent.data, done: false },
|
||||
};
|
||||
} else if (updateEvent.type === "tts-end") {
|
||||
run = { ...run, tts: { ...run.tts!, ...updateEvent.data } };
|
||||
run = {
|
||||
...run,
|
||||
tts: { ...run.tts!, ...updateEvent.data, done: true },
|
||||
};
|
||||
} else if (updateEvent.type === "run-end") {
|
||||
run = { ...run, stage: "done" };
|
||||
unsubProm.then((unsub) => unsub());
|
||||
|
@ -3,6 +3,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeAttributeNameDisplay } from "../../../../common/entity/compute_attribute_display";
|
||||
import { computeStateDisplay } from "../../../../common/entity/compute_state_display";
|
||||
import { stateActive } from "../../../../common/entity/state_active";
|
||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||
import "../../../../components/ha-control-select";
|
||||
import type { ControlSelectOption } from "../../../../components/ha-control-select";
|
||||
@ -26,20 +27,25 @@ export class HaMoreInfoFanSpeed extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateObj!: FanEntity;
|
||||
|
||||
@state() value?: number;
|
||||
@state() sliderValue?: number;
|
||||
|
||||
@state() speedValue?: FanSpeed;
|
||||
|
||||
protected updated(changedProp: Map<string | number | symbol, unknown>): void {
|
||||
if (changedProp.has("stateObj")) {
|
||||
this.value =
|
||||
this.stateObj.attributes.percentage != null
|
||||
? Math.max(Math.round(this.stateObj.attributes.percentage), 1)
|
||||
: undefined;
|
||||
const percentage = stateActive(this.stateObj)
|
||||
? this.stateObj.attributes.percentage ?? 0
|
||||
: 0;
|
||||
this.sliderValue = Math.max(Math.round(percentage), 0);
|
||||
this.speedValue = fanPercentageToSpeed(this.stateObj, percentage);
|
||||
}
|
||||
}
|
||||
|
||||
private _speedValueChanged(ev: CustomEvent) {
|
||||
const speed = (ev.detail as any).value as FanSpeed;
|
||||
|
||||
this.speedValue = speed;
|
||||
|
||||
const percentage = fanSpeedToPercentage(this.stateObj, speed);
|
||||
|
||||
this.hass.callService("fan", "set_percentage", {
|
||||
@ -52,6 +58,8 @@ export class HaMoreInfoFanSpeed extends LitElement {
|
||||
const value = (ev.detail as any).value;
|
||||
if (isNaN(value)) return;
|
||||
|
||||
this.sliderValue = value;
|
||||
|
||||
this.hass.callService("fan", "set_percentage", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
percentage: value,
|
||||
@ -88,16 +96,11 @@ export class HaMoreInfoFanSpeed extends LitElement {
|
||||
})
|
||||
).reverse();
|
||||
|
||||
const speed = fanPercentageToSpeed(
|
||||
this.stateObj,
|
||||
this.stateObj.attributes.percentage ?? 0
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-control-select
|
||||
vertical
|
||||
.options=${options}
|
||||
.value=${speed}
|
||||
.value=${this.speedValue}
|
||||
@value-changed=${this._speedValueChanged}
|
||||
.ariaLabel=${computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
@ -119,7 +122,7 @@ export class HaMoreInfoFanSpeed extends LitElement {
|
||||
vertical
|
||||
min="0"
|
||||
max="100"
|
||||
.value=${this.value}
|
||||
.value=${this.sliderValue}
|
||||
.step=${this.stateObj.attributes.percentage_step ?? 1}
|
||||
@value-changed=${this._valueChanged}
|
||||
.ariaLabel=${computeAttributeNameDisplay(
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
computeAttributeValueDisplay,
|
||||
} from "../../../common/entity/compute_attribute_display";
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-attributes";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
@ -119,7 +120,7 @@ class MoreInfoFan extends LitElement {
|
||||
const liveValue = this._liveSpeed;
|
||||
|
||||
const forcedState =
|
||||
this._liveSpeed != null ? (this._liveSpeed ? "on" : "off") : undefined;
|
||||
liveValue != null ? (liveValue ? "on" : "off") : undefined;
|
||||
|
||||
const stateDisplay = computeStateDisplay(
|
||||
this.hass.localize,
|
||||
@ -135,7 +136,7 @@ class MoreInfoFan extends LitElement {
|
||||
liveValue
|
||||
);
|
||||
|
||||
if (positionStateDisplay) {
|
||||
if (positionStateDisplay && (stateActive(this.stateObj!) || liveValue)) {
|
||||
return positionStateDisplay;
|
||||
}
|
||||
return stateDisplay;
|
||||
@ -273,7 +274,6 @@ class MoreInfoFan extends LitElement {
|
||||
supportsPresetMode && this.stateObj.attributes.preset_modes
|
||||
? html`
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
@action=${this._handlePresetMode}
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
|
@ -173,7 +173,6 @@ class MoreInfoLight extends LitElement {
|
||||
${supportsEffects && this.stateObj.attributes.effect_list
|
||||
? html`
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
@action=${this._handleEffectButton}
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
|
@ -12,6 +12,8 @@ import {
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-attributes";
|
||||
import "../../../components/ha-icon";
|
||||
@ -113,11 +115,19 @@ class MoreInfoVacuum extends LitElement {
|
||||
</span>
|
||||
<span>
|
||||
<strong>
|
||||
${stateObj.attributes.status ||
|
||||
this.hass.localize(
|
||||
`component.vacuum.entity_component._.state.${stateObj.state}`
|
||||
${computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.entities,
|
||||
"status"
|
||||
) ||
|
||||
stateObj.state}
|
||||
computeStateDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
)}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -451,6 +451,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
--dialog-surface-position: static;
|
||||
--dialog-content-position: static;
|
||||
--dialog-content-padding: 0;
|
||||
--chart-base-position: static;
|
||||
}
|
||||
|
||||
ha-header-bar {
|
||||
|
@ -99,6 +99,8 @@ class HassSubpage extends LitElement {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host([narrow]) {
|
||||
@ -152,7 +154,7 @@ class HassSubpage extends LitElement {
|
||||
}
|
||||
|
||||
#fab {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
right: calc(16px + env(safe-area-inset-right));
|
||||
bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
z-index: 1;
|
||||
|
@ -182,8 +182,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
: html`
|
||||
<ha-button-menu
|
||||
slot="icons"
|
||||
fixed
|
||||
corner="BOTTOM_START"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
>
|
||||
|
@ -128,11 +128,7 @@ export default class HaAutomationAction extends LitElement {
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<ha-button-menu
|
||||
fixed
|
||||
@action=${this._addAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-button-menu @action=${this._addAction} .disabled=${this.disabled}>
|
||||
<ha-button
|
||||
slot="trigger"
|
||||
outlined
|
||||
|
@ -116,8 +116,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
: html`
|
||||
<ha-button-menu
|
||||
slot="icons"
|
||||
fixed
|
||||
corner="BOTTOM_START"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
>
|
||||
|
@ -180,11 +180,7 @@ export default class HaAutomationCondition extends LitElement {
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<ha-button-menu
|
||||
fixed
|
||||
@action=${this._addCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-button-menu @action=${this._addCondition} .disabled=${this.disabled}>
|
||||
<ha-button
|
||||
slot="trigger"
|
||||
outlined
|
||||
|
@ -141,7 +141,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
|
@ -287,7 +287,6 @@ class HaAutomationPicker extends LitElement {
|
||||
></ha-icon-button>
|
||||
<ha-button-related-filter-menu
|
||||
slot="filter-menu"
|
||||
corner="BOTTOM_START"
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.value=${this._filterValue}
|
||||
|
@ -113,7 +113,7 @@ export class HaAutomationTrace extends LitElement {
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
|
@ -140,8 +140,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
: html`
|
||||
<ha-button-menu
|
||||
slot="icons"
|
||||
fixed
|
||||
corner="BOTTOM_START"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
>
|
||||
|
@ -168,7 +168,6 @@ class CloudAlexa extends LitElement {
|
||||
${!emptyFilter
|
||||
? html`${iconButton}`
|
||||
: html`<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
.entityId=${stateObj.entity_id}
|
||||
@action=${this._exposeChanged}
|
||||
>
|
||||
@ -225,7 +224,7 @@ class CloudAlexa extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass!.localize("ui.panel.config.cloud.alexa.title")}
|
||||
>
|
||||
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
|
@ -228,7 +228,6 @@ class CloudGoogleAssistant extends LitElement {
|
||||
${!emptyFilter
|
||||
? html`${iconButton}`
|
||||
: html`<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
.entityId=${entity.entity_id}
|
||||
@action=${this._exposeChanged}
|
||||
>
|
||||
@ -302,7 +301,7 @@ class CloudGoogleAssistant extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.header=${this.hass!.localize("ui.panel.config.cloud.google.title")}
|
||||
.narrow=${this.narrow}>
|
||||
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
|
@ -69,7 +69,7 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
.path=${mdiUpdate}
|
||||
@click=${this._checkUpdates}
|
||||
></ha-icon-button>
|
||||
<ha-button-menu corner="BOTTOM_START" multi>
|
||||
<ha-button-menu multi>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
|
@ -194,11 +194,7 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMagnify}
|
||||
@click=${this._showQuickBar}
|
||||
></ha-icon-button>
|
||||
<ha-button-menu
|
||||
slot="actionItems"
|
||||
corner="BOTTOM_START"
|
||||
@action=${this._handleMenuAction}
|
||||
>
|
||||
<ha-button-menu slot="actionItems" @action=${this._handleMenuAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
|
@ -748,7 +748,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
|
||||
${actions.length
|
||||
? html`
|
||||
<ha-button-menu corner="BOTTOM_START">
|
||||
<ha-button-menu>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
|
@ -448,7 +448,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
<ha-button-menu slot="filter-menu" corner="BOTTOM_START" multi>
|
||||
<ha-button-menu slot="filter-menu" multi>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize(
|
||||
|
@ -620,7 +620,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-button-menu slot="filter-menu" corner="BOTTOM_START" multi>
|
||||
<ha-button-menu slot="filter-menu" multi>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize(
|
||||
|
@ -353,12 +353,9 @@ class HaPanelConfig extends HassRouterPage {
|
||||
tag: "ha-config-areas",
|
||||
load: () => import("./areas/ha-config-areas"),
|
||||
},
|
||||
voice_assistant: {
|
||||
tag: "assist-pipeline-debug",
|
||||
load: () =>
|
||||
import(
|
||||
"./integrations/integration-panels/voice_assistant/assist/assist-pipeline-debug"
|
||||
),
|
||||
"voice-assistants": {
|
||||
tag: "ha-config-voice-assistants",
|
||||
load: () => import("./voice-assistants/ha-config-voice-assistants"),
|
||||
},
|
||||
automation: {
|
||||
tag: "ha-config-automation",
|
||||
|
@ -60,7 +60,7 @@ export class HaConfigFlowCard extends LitElement {
|
||||
}`
|
||||
)}
|
||||
></mwc-button>
|
||||
<ha-button-menu corner="BOTTOM_START">
|
||||
<ha-button-menu>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
|
@ -382,7 +382,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
? html`<span class="badge">${disabledCount}</span>`
|
||||
: ""}
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
multi
|
||||
@action=${this._handleMenuAction}
|
||||
@click=${this._preventDefault}
|
||||
|
@ -370,7 +370,7 @@ export class HaIntegrationCard extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<ha-button-menu corner="BOTTOM_START">
|
||||
<ha-button-menu>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
|
@ -12,7 +12,7 @@ export class HaIntegrationOverflowMenu extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-button-menu activatable corner="BOTTOM_START">
|
||||
<ha-button-menu activatable>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
|
@ -69,7 +69,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
|
||||
return html`
|
||||
<hass-subpage .narrow=${this.narrow} .hass=${this.hass} header="Thread">
|
||||
<ha-button-menu slot="toolbar-icon" corner="BOTTOM_START">
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
@ -186,7 +186,6 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
<span slot="secondary">${router.server}</span>
|
||||
${router.extended_address === this._otbrInfo?.extended_address
|
||||
? html`<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
slot="meta"
|
||||
@action=${this._handleRouterAction}
|
||||
>
|
||||
|
@ -1,20 +1,25 @@
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import "../../../../../../components/ha-button";
|
||||
import {
|
||||
PipelineRun,
|
||||
runPipelineFromText,
|
||||
PipelineRunOptions,
|
||||
runVoiceAssistantPipeline,
|
||||
} from "../../../../../../data/voice_assistant";
|
||||
import "../../../../../../layouts/hass-subpage";
|
||||
import "../../../../../../components/ha-formfield";
|
||||
import "../../../../../../components/ha-checkbox";
|
||||
import { haStyle } from "../../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../../types";
|
||||
import { showPromptDialog } from "../../../../../../dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../../../../dialogs/generic/show-dialog-box";
|
||||
import "./assist-render-pipeline-run";
|
||||
import type { HaCheckbox } from "../../../../../../components/ha-checkbox";
|
||||
import type { HaTextField } from "../../../../../../components/ha-textfield";
|
||||
import "../../../../../../components/ha-textfield";
|
||||
import { fileDownload } from "../../../../../../util/file_download";
|
||||
|
||||
@customElement("assist-pipeline-debug")
|
||||
export class AssistPipelineDebug extends LitElement {
|
||||
@ -24,8 +29,6 @@ export class AssistPipelineDebug extends LitElement {
|
||||
|
||||
@state() private _pipelineRuns: PipelineRun[] = [];
|
||||
|
||||
@state() private _stopRecording?: () => void;
|
||||
|
||||
@query("#continue-conversation")
|
||||
private _continueConversationCheckbox!: HaCheckbox;
|
||||
|
||||
@ -36,6 +39,8 @@ export class AssistPipelineDebug extends LitElement {
|
||||
|
||||
@state() private _finished = false;
|
||||
|
||||
@state() private _languageOverride?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
@ -52,8 +57,18 @@ export class AssistPipelineDebug extends LitElement {
|
||||
>
|
||||
Clear
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="toolbar-icon"
|
||||
@click=${this._downloadConversation}
|
||||
>
|
||||
Download
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
: html`
|
||||
<ha-button slot="toolbar-icon" @click=${this._setLanguage}>
|
||||
Set Language
|
||||
</ha-button>
|
||||
`}
|
||||
|
||||
<div class="content">
|
||||
<div class="start-row">
|
||||
@ -81,6 +96,12 @@ export class AssistPipelineDebug extends LitElement {
|
||||
Send
|
||||
</ha-button>
|
||||
`
|
||||
: this._finished
|
||||
? html`
|
||||
<ha-button @click=${this._runAudioPipeline}>
|
||||
Continue talking
|
||||
</ha-button>
|
||||
`
|
||||
: html`
|
||||
<ha-formfield label="Continue conversation">
|
||||
<ha-checkbox
|
||||
@ -106,53 +127,6 @@ export class AssistPipelineDebug extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (
|
||||
!changedProperties.has("_pipelineRuns") ||
|
||||
this._pipelineRuns.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRun = this._pipelineRuns[0];
|
||||
|
||||
if (currentRun.init_options.start_stage !== "stt") {
|
||||
if (["error", "done"].includes(currentRun.stage)) {
|
||||
this._finished = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentRun.stage === "stt" && this._audioBuffer) {
|
||||
// Send the buffer over the WS to the STT engine.
|
||||
for (const buffer of this._audioBuffer) {
|
||||
this._sendAudioChunk(buffer);
|
||||
}
|
||||
this._audioBuffer = undefined;
|
||||
}
|
||||
|
||||
if (currentRun.stage !== "stt" && this._stopRecording) {
|
||||
this._stopRecording();
|
||||
}
|
||||
|
||||
if (currentRun.stage === "done") {
|
||||
const url = currentRun.tts!.tts_output!.url;
|
||||
const audio = new Audio(url);
|
||||
audio.addEventListener("ended", () => {
|
||||
if (this._continueConversationCheckbox.checked) {
|
||||
this._runAudioPipeline();
|
||||
} else {
|
||||
this._finished = true;
|
||||
}
|
||||
});
|
||||
audio.play();
|
||||
} else if (currentRun.stage === "error") {
|
||||
this._finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
private get conversationId(): string | null {
|
||||
return this._pipelineRuns.length === 0
|
||||
? null
|
||||
@ -177,26 +151,19 @@ export class AssistPipelineDebug extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
let added = false;
|
||||
runPipelineFromText(
|
||||
this.hass,
|
||||
await this._doRunPipeline(
|
||||
(run) => {
|
||||
if (textfield && ["done", "error"].includes(run.stage)) {
|
||||
textfield.value = "";
|
||||
}
|
||||
|
||||
if (added) {
|
||||
this._pipelineRuns = [run, ...this._pipelineRuns.slice(1)];
|
||||
} else {
|
||||
this._pipelineRuns = [run, ...this._pipelineRuns];
|
||||
added = true;
|
||||
if (["done", "error"].includes(run.stage)) {
|
||||
this._finished = true;
|
||||
if (textfield) {
|
||||
textfield.value = "";
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
start_stage: "intent",
|
||||
end_stage: "intent",
|
||||
input: { text },
|
||||
conversation_id: this.conversationId,
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -204,7 +171,13 @@ export class AssistPipelineDebug extends LitElement {
|
||||
private async _runAudioPipeline() {
|
||||
// @ts-ignore-next-line
|
||||
const context = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
let stream: MediaStream;
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
await context.audioWorklet.addModule(
|
||||
new URL("./recorder.worklet.js", import.meta.url)
|
||||
);
|
||||
@ -213,47 +186,111 @@ export class AssistPipelineDebug extends LitElement {
|
||||
const recorder = new AudioWorkletNode(context, "recorder.worklet");
|
||||
|
||||
this.hass.connection.socket!.binaryType = "arraybuffer";
|
||||
this._stopRecording = () => {
|
||||
|
||||
let run: PipelineRun | undefined;
|
||||
|
||||
let stopRecording: (() => void) | undefined = () => {
|
||||
stopRecording = undefined;
|
||||
// We're currently STTing, so finish audio
|
||||
if (run?.stage === "stt" && run.stt!.done === false) {
|
||||
if (this._audioBuffer) {
|
||||
for (const chunk of this._audioBuffer) {
|
||||
this._sendAudioChunk(chunk);
|
||||
}
|
||||
}
|
||||
// Send empty message to indicate we're done streaming.
|
||||
this._sendAudioChunk(new Int16Array());
|
||||
}
|
||||
this._audioBuffer = undefined;
|
||||
stream.getTracks()[0].stop();
|
||||
context.close();
|
||||
this._stopRecording = undefined;
|
||||
this._audioBuffer = undefined;
|
||||
// Send empty message to indicate we're done streaming.
|
||||
this._sendAudioChunk(new Int16Array());
|
||||
};
|
||||
this._audioBuffer = [];
|
||||
source.connect(recorder).connect(context.destination);
|
||||
recorder.port.onmessage = (e) => {
|
||||
if (!stopRecording) {
|
||||
return;
|
||||
}
|
||||
if (this._audioBuffer) {
|
||||
this._audioBuffer.push(e.data);
|
||||
return;
|
||||
} else {
|
||||
this._sendAudioChunk(e.data);
|
||||
}
|
||||
if (this._pipelineRuns[0].stage !== "stt") {
|
||||
return;
|
||||
}
|
||||
this._sendAudioChunk(e.data);
|
||||
};
|
||||
|
||||
this._finished = false;
|
||||
let added = false;
|
||||
runPipelineFromText(
|
||||
this.hass,
|
||||
(run) => {
|
||||
if (added) {
|
||||
this._pipelineRuns = [run, ...this._pipelineRuns.slice(1)];
|
||||
} else {
|
||||
this._pipelineRuns = [run, ...this._pipelineRuns];
|
||||
added = true;
|
||||
await this._doRunPipeline(
|
||||
(updatedRun) => {
|
||||
run = updatedRun;
|
||||
|
||||
// When we start STT stage, the WS has a binary handler
|
||||
if (updatedRun.stage === "stt" && this._audioBuffer) {
|
||||
// Send the buffer over the WS to the STT engine.
|
||||
for (const buffer of this._audioBuffer) {
|
||||
this._sendAudioChunk(buffer);
|
||||
}
|
||||
this._audioBuffer = undefined;
|
||||
}
|
||||
|
||||
// Stop recording if the server is done with STT stage
|
||||
if (!["ready", "stt"].includes(updatedRun.stage) && stopRecording) {
|
||||
stopRecording();
|
||||
}
|
||||
|
||||
// Play audio when we're done.
|
||||
if (updatedRun.stage === "done") {
|
||||
const url = updatedRun.tts!.tts_output!.url;
|
||||
const audio = new Audio(url);
|
||||
audio.addEventListener("ended", () => {
|
||||
if (this._continueConversationCheckbox.checked) {
|
||||
this._runAudioPipeline();
|
||||
} else {
|
||||
this._finished = true;
|
||||
}
|
||||
});
|
||||
audio.play();
|
||||
} else if (updatedRun.stage === "error") {
|
||||
this._finished = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
start_stage: "stt",
|
||||
end_stage: "tts",
|
||||
conversation_id: this.conversationId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _doRunPipeline(
|
||||
callback: (event: PipelineRun) => void,
|
||||
options: PipelineRunOptions
|
||||
) {
|
||||
this._finished = false;
|
||||
let added = false;
|
||||
try {
|
||||
await runVoiceAssistantPipeline(
|
||||
this.hass,
|
||||
(updatedRun) => {
|
||||
if (added) {
|
||||
this._pipelineRuns = [updatedRun, ...this._pipelineRuns.slice(1)];
|
||||
} else {
|
||||
this._pipelineRuns = [updatedRun, ...this._pipelineRuns];
|
||||
added = true;
|
||||
}
|
||||
callback(updatedRun);
|
||||
},
|
||||
{
|
||||
...options,
|
||||
language: this._languageOverride,
|
||||
conversation_id: this.conversationId,
|
||||
}
|
||||
);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
title: "Error starting pipeline",
|
||||
text: err.message || err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _sendAudioChunk(chunk: Int16Array) {
|
||||
// Turn into 8 bit so we can prefix our handler ID.
|
||||
const data = new Uint8Array(1 + chunk.length * 2);
|
||||
@ -273,6 +310,27 @@ export class AssistPipelineDebug extends LitElement {
|
||||
this._pipelineRuns = [];
|
||||
}
|
||||
|
||||
private _downloadConversation() {
|
||||
fileDownload(
|
||||
`data:text/plain;charset=utf-8,${encodeURIComponent(
|
||||
JSON.stringify(this._pipelineRuns, null, 2)
|
||||
)}`,
|
||||
`conversation.json`
|
||||
);
|
||||
}
|
||||
|
||||
private async _setLanguage() {
|
||||
const language = await showPromptDialog(this, {
|
||||
title: "Language override",
|
||||
inputLabel: "Language",
|
||||
inputType: "text",
|
||||
confirmText: "Set",
|
||||
});
|
||||
if (language) {
|
||||
this._languageOverride = language;
|
||||
}
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
|
@ -50,9 +50,11 @@ const maybeRenderError = (
|
||||
return "";
|
||||
}
|
||||
|
||||
return html`<ha-alert alert-type="error">
|
||||
${run.error!.message} (${run.error!.code})
|
||||
</ha-alert>`;
|
||||
return html`
|
||||
<ha-alert alert-type="error">
|
||||
${run.error!.message} (${run.error!.code})
|
||||
</ha-alert>
|
||||
`;
|
||||
};
|
||||
|
||||
const renderProgress = (
|
||||
@ -76,10 +78,9 @@ const renderProgress = (
|
||||
}
|
||||
|
||||
if (!finishEvent) {
|
||||
return html`<ha-circular-progress
|
||||
size="tiny"
|
||||
active
|
||||
></ha-circular-progress>`;
|
||||
return html`
|
||||
<ha-circular-progress size="tiny" active></ha-circular-progress>
|
||||
`;
|
||||
}
|
||||
|
||||
const duration =
|
||||
@ -109,7 +110,7 @@ const dataMinusKeysRender = (
|
||||
const result = {};
|
||||
let render = false;
|
||||
for (const key in data) {
|
||||
if (key in keys) {
|
||||
if (key in keys || key === "done") {
|
||||
continue;
|
||||
}
|
||||
render = true;
|
||||
|
@ -115,7 +115,7 @@ export class HaConfigLogs extends LitElement {
|
||||
${isComponentLoaded(this.hass, "hassio") &&
|
||||
this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-button
|
||||
slot="trigger"
|
||||
.label=${this._logProviders.find(
|
||||
|
@ -267,11 +267,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
>
|
||||
${this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
slot="toolbar-icon"
|
||||
activatable
|
||||
>
|
||||
<ha-button-menu slot="toolbar-icon" activatable>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
|
@ -248,7 +248,7 @@ export class HassioNetwork extends LitElement {
|
||||
</ha-circular-progress>`
|
||||
: this.hass.localize("ui.common.save")}
|
||||
</mwc-button>
|
||||
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
|
||||
<ha-button-menu @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${"ui.common.menu"}
|
||||
|
@ -79,7 +79,7 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
|
||||
.header=${this.hass.localize("ui.panel.config.repairs.caption")}
|
||||
>
|
||||
<div slot="toolbar-icon">
|
||||
<ha-button-menu corner="BOTTOM_START" multi>
|
||||
<ha-button-menu multi>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
|
@ -225,7 +225,6 @@ class HaSceneDashboard extends LitElement {
|
||||
></ha-icon-button>
|
||||
<ha-button-related-filter-menu
|
||||
slot="filter-menu"
|
||||
corner="BOTTOM_START"
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.value=${this._filterValue}
|
||||
|
@ -228,7 +228,6 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
: this.hass.localize("ui.panel.config.scene.editor.default_name")}
|
||||
>
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
slot="toolbar-icon"
|
||||
@action=${this._handleMenuAction}
|
||||
activatable
|
||||
|
@ -191,7 +191,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
|
@ -227,7 +227,6 @@ class HaScriptPicker extends LitElement {
|
||||
></ha-icon-button>
|
||||
<ha-button-related-filter-menu
|
||||
slot="filter-menu"
|
||||
corner="BOTTOM_START"
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.value=${this._filterValue}
|
||||
|
@ -116,7 +116,7 @@ export class HaScriptTrace extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
|
@ -44,7 +44,7 @@ class HaConfigSectionStorage extends LitElement {
|
||||
>
|
||||
${this._hostInfo
|
||||
? html`
|
||||
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
|
@ -0,0 +1,41 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import {
|
||||
HassRouterPage,
|
||||
RouterOptions,
|
||||
} from "../../../layouts/hass-router-page";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("ha-config-voice-assistants")
|
||||
class HaConfigVoiceAssistants extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property() public isWide!: boolean;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "debug",
|
||||
routes: {
|
||||
debug: {
|
||||
tag: "assist-pipeline-debug",
|
||||
load: () =>
|
||||
import(
|
||||
"../integrations/integration-panels/voice_assistant/assist/assist-pipeline-debug"
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
protected updatePageEl(pageEl) {
|
||||
pageEl.hass = this.hass;
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.isWide = this.isWide;
|
||||
pageEl.route = this.routeTail;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-voice-assistants": HaConfigVoiceAssistants;
|
||||
}
|
||||
}
|
@ -214,7 +214,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
}
|
||||
|
||||
if (domain === "fan" && stateActive(stateObj)) {
|
||||
if (domain === "fan") {
|
||||
const speedStateDisplay = computeFanSpeedStateDisplay(
|
||||
stateObj as FanEntity,
|
||||
this.hass!.locale
|
||||
@ -231,12 +231,11 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
this.hass!.entities
|
||||
);
|
||||
|
||||
if (domain === "cover" && stateActive(stateObj)) {
|
||||
if (domain === "cover") {
|
||||
const positionStateDisplay = computeCoverPositionStateDisplay(
|
||||
stateObj as CoverEntity,
|
||||
this.hass!.locale
|
||||
);
|
||||
|
||||
if (positionStateDisplay) {
|
||||
return `${stateDisplay} ⸱ ${positionStateDisplay}`;
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ export class HuiCardOptions extends LitElement {
|
||||
@click=${this._cardUp}
|
||||
?disabled=${this.path![1] === 0}
|
||||
></ha-icon-button>
|
||||
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
|
||||
<ha-button-menu @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize(
|
||||
|
@ -93,7 +93,6 @@ export class HuiViewEditor extends LitElement {
|
||||
const schema = this._schema(this.hass.localize);
|
||||
|
||||
const data = {
|
||||
theme: "Backend-selected",
|
||||
...this._config,
|
||||
type: this._type,
|
||||
};
|
||||
|
@ -165,7 +165,7 @@ class HUIRoot extends LitElement {
|
||||
.path=${mdiHelpCircle}
|
||||
></ha-icon-button>
|
||||
</a>
|
||||
<ha-button-menu corner="BOTTOM_START">
|
||||
<ha-button-menu>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize(
|
||||
@ -319,10 +319,7 @@ class HUIRoot extends LitElement {
|
||||
: ""}
|
||||
${this._showButtonMenu
|
||||
? html`
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
slot="actionItems"
|
||||
>
|
||||
<ha-button-menu slot="actionItems">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize(
|
||||
@ -1060,6 +1057,7 @@ class HUIRoot extends LitElement {
|
||||
);
|
||||
overflow: auto;
|
||||
transform: translateZ(0);
|
||||
display: flex;
|
||||
}
|
||||
/**
|
||||
* In edit mode we have the tab bar on a new line *
|
||||
@ -1082,6 +1080,7 @@ class HUIRoot extends LitElement {
|
||||
* https://github.com/home-assistant/home-assistant-polymer/pull/3806
|
||||
*/
|
||||
flex: 1 1 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
display: block;
|
||||
|
@ -4,6 +4,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-control-select";
|
||||
import type { ControlSelectOption } from "../../../components/ha-control-select";
|
||||
@ -12,6 +13,7 @@ import { UNAVAILABLE } from "../../../data/entity";
|
||||
import {
|
||||
computeFanSpeedCount,
|
||||
computeFanSpeedIcon,
|
||||
FanEntity,
|
||||
FanEntityFeature,
|
||||
fanPercentageToSpeed,
|
||||
FanSpeed,
|
||||
@ -34,7 +36,7 @@ export const supportsFanSpeedTileFeature = (stateObj: HassEntity) => {
|
||||
class HuiFanSpeedTileFeature extends LitElement implements LovelaceTileFeature {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
@property({ attribute: false }) public stateObj?: FanEntity;
|
||||
|
||||
@state() private _config?: FanSpeedTileFeatureConfig;
|
||||
|
||||
@ -79,6 +81,10 @@ class HuiFanSpeedTileFeature extends LitElement implements LovelaceTileFeature {
|
||||
|
||||
const speedCount = computeFanSpeedCount(this.stateObj);
|
||||
|
||||
const percentage = stateActive(this.stateObj)
|
||||
? this.stateObj.attributes.percentage ?? 0
|
||||
: 0;
|
||||
|
||||
if (speedCount <= FAN_SPEED_COUNT_MAX_FOR_BUTTONS) {
|
||||
const options = FAN_SPEEDS[speedCount]!.map<ControlSelectOption>(
|
||||
(speed) => ({
|
||||
@ -88,10 +94,7 @@ class HuiFanSpeedTileFeature extends LitElement implements LovelaceTileFeature {
|
||||
})
|
||||
);
|
||||
|
||||
const speed = fanPercentageToSpeed(
|
||||
this.stateObj,
|
||||
this.stateObj.attributes.percentage ?? 0
|
||||
);
|
||||
const speed = fanPercentageToSpeed(this.stateObj, percentage);
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
@ -113,15 +116,12 @@ class HuiFanSpeedTileFeature extends LitElement implements LovelaceTileFeature {
|
||||
`;
|
||||
}
|
||||
|
||||
const percentage =
|
||||
this.stateObj.attributes.percentage != null
|
||||
? Math.max(Math.round(this.stateObj.attributes.percentage), 0)
|
||||
: undefined;
|
||||
const value = Math.max(Math.round(percentage), 0);
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
<ha-control-slider
|
||||
.value=${percentage}
|
||||
.value=${value}
|
||||
min="0"
|
||||
max="100"
|
||||
.step=${this.stateObj.attributes.percentage_step ?? 1}
|
||||
|
@ -292,7 +292,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
stateObj &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET)
|
||||
? html`
|
||||
<ha-button-menu corner="BOTTOM_START" y="0" x="76">
|
||||
<ha-button-menu y="0" x="76">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiVolumeHigh}
|
||||
@ -310,7 +310,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
: ""
|
||||
}
|
||||
|
||||
<ha-button-menu corner="BOTTOM_START">
|
||||
<ha-button-menu >
|
||||
${
|
||||
this.narrow
|
||||
? html`
|
||||
|
@ -37,6 +37,7 @@ class HaPickDashboardRow extends LitElement {
|
||||
.disabled=${!this._dashboards?.length}
|
||||
.value=${this.hass.defaultPanel}
|
||||
@selected=${this._dashboardChanged}
|
||||
naturalMenuWidth
|
||||
>
|
||||
<mwc-list-item value="lovelace">
|
||||
${this.hass.localize(
|
||||
|
@ -30,6 +30,7 @@ class FirstWeekdayRow extends LitElement {
|
||||
.disabled=${this.hass.locale === undefined}
|
||||
.value=${this.hass.locale.first_weekday}
|
||||
@selected=${this._handleFormatSelection}
|
||||
naturalMenuWidth
|
||||
>
|
||||
${[
|
||||
FirstWeekday.language,
|
||||
|
@ -39,6 +39,7 @@ export class HaPickLanguageRow extends LitElement {
|
||||
)}
|
||||
.value=${this.hass.locale.language}
|
||||
@selected=${this._languageSelectionChanged}
|
||||
naturalMenuWidth
|
||||
>
|
||||
${this._languages.map(
|
||||
(language) => html`<mwc-list-item
|
||||
|
@ -31,6 +31,7 @@ class NumberFormatRow extends LitElement {
|
||||
.disabled=${this.hass.locale === undefined}
|
||||
.value=${this.hass.locale.number_format}
|
||||
@selected=${this._handleFormatSelection}
|
||||
naturalMenuWidth
|
||||
>
|
||||
${Object.values(NumberFormat).map((format) => {
|
||||
const formattedNumber = formatNumber(1234567.89, {
|
||||
|
@ -23,6 +23,9 @@ import {
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
|
||||
const BACKEND_SELECTED_THEME = "Backend-selected";
|
||||
const DEFAULT_THEME = "default";
|
||||
|
||||
@customElement("ha-pick-theme-row")
|
||||
export class HaPickThemeRow extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@ -65,16 +68,24 @@ export class HaPickThemeRow extends LitElement {
|
||||
<ha-select
|
||||
.label=${this.hass.localize("ui.panel.profile.themes.dropdown_label")}
|
||||
.disabled=${!hasThemes}
|
||||
.value=${this.hass.selectedTheme?.theme || "Backend-selected"}
|
||||
.value=${this.hass.selectedTheme?.theme || BACKEND_SELECTED_THEME}
|
||||
@selected=${this._handleThemeSelection}
|
||||
naturalMenuWidth
|
||||
>
|
||||
<mwc-list-item .value=${BACKEND_SELECTED_THEME}>
|
||||
${this.hass.localize("ui.panel.profile.themes.backend-selected")}
|
||||
</mwc-list-item>
|
||||
<mwc-list-item .value=${DEFAULT_THEME}>
|
||||
${this.hass.localize("ui.panel.profile.themes.default")}
|
||||
</mwc-list-item>
|
||||
${this._themeNames.map(
|
||||
(theme) =>
|
||||
html`<mwc-list-item .value=${theme}>${theme}</mwc-list-item>`
|
||||
(theme) => html`
|
||||
<mwc-list-item .value=${theme}>${theme}</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
</ha-settings-row>
|
||||
${curTheme === "default" || this._supportsModeSelection(curTheme)
|
||||
${curTheme === DEFAULT_THEME || this._supportsModeSelection(curTheme)
|
||||
? html` <div class="inputs">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
@ -114,7 +125,7 @@ export class HaPickThemeRow extends LitElement {
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
${curTheme === "default"
|
||||
${curTheme === DEFAULT_THEME
|
||||
? html`<div class="color-pickers">
|
||||
<ha-textfield
|
||||
.value=${themeSettings?.primaryColor ||
|
||||
@ -154,9 +165,7 @@ export class HaPickThemeRow extends LitElement {
|
||||
(!oldHass || oldHass.themes.themes !== this.hass.themes.themes);
|
||||
|
||||
if (themesChanged) {
|
||||
this._themeNames = ["Backend-selected", "default"].concat(
|
||||
Object.keys(this.hass.themes.themes).sort()
|
||||
);
|
||||
this._themeNames = Object.keys(this.hass.themes.themes).sort();
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,7 +207,7 @@ export class HaPickThemeRow extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (theme === "Backend-selected") {
|
||||
if (theme === BACKEND_SELECTED_THEME) {
|
||||
if (this.hass.selectedTheme?.theme) {
|
||||
fireEvent(this, "settheme", {
|
||||
theme: "",
|
||||
|
@ -32,6 +32,7 @@ class TimeFormatRow extends LitElement {
|
||||
.disabled=${this.hass.locale === undefined}
|
||||
.value=${this.hass.locale.time_format}
|
||||
@selected=${this._handleFormatSelection}
|
||||
naturalMenuWidth
|
||||
>
|
||||
${Object.values(TimeFormat).map((format) => {
|
||||
const formattedTime = formatTime(date, {
|
||||
|
@ -4711,7 +4711,9 @@
|
||||
},
|
||||
"primary_color": "Primary color",
|
||||
"accent_color": "Accent color",
|
||||
"reset": "Reset"
|
||||
"reset": "Reset",
|
||||
"backend-selected": "Use backend preferred theme",
|
||||
"default": "Default"
|
||||
},
|
||||
"dashboard": {
|
||||
"header": "Dashboard",
|
||||
|
@ -16,7 +16,9 @@ const handler = {
|
||||
console.warn(message);
|
||||
document
|
||||
.querySelector("home-assistant")
|
||||
.dispatchEvent(new CustomEvent("write_log", { detail: { message } }));
|
||||
.dispatchEvent(
|
||||
new CustomEvent("write_log", { detail: { message, level: "warning" } })
|
||||
);
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
apply: function (target, thisArg, argumentsList) {
|
||||
@ -24,7 +26,9 @@ const handler = {
|
||||
console.warn(message);
|
||||
document
|
||||
.querySelector("home-assistant")
|
||||
.dispatchEvent(new CustomEvent("write_log", { detail: { message } }));
|
||||
.dispatchEvent(
|
||||
new CustomEvent("write_log", { detail: { message, level: "warning" } })
|
||||
);
|
||||
return Reflect.apply(target, thisArg, argumentsList);
|
||||
},
|
||||
};
|
||||
|
30
yarn.lock
30
yarn.lock
@ -5179,10 +5179,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@webcomponents/scoped-custom-element-registry@npm:0.0.8":
|
||||
version: 0.0.8
|
||||
resolution: "@webcomponents/scoped-custom-element-registry@npm:0.0.8"
|
||||
checksum: b758bd34723834eedeabe53decadeaa3a655a0fb6b264f1ad55d9fb617f1cff44c72f7722e78cfb953a82c06f85c1e640929d65e775df85202c34419091d2cdf
|
||||
"@webcomponents/scoped-custom-element-registry@npm:0.0.9":
|
||||
version: 0.0.9
|
||||
resolution: "@webcomponents/scoped-custom-element-registry@npm:0.0.9"
|
||||
checksum: 02595d7b184a04fab16b12599f80a3e405ed9cee347e42079423bf4b7d4c794201d1553d818c0663d83c8b047a77eec7b32318267927673bd4218eb0f75a1b5e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -5193,10 +5193,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@webcomponents/webcomponentsjs@npm:2.7.0":
|
||||
version: 2.7.0
|
||||
resolution: "@webcomponents/webcomponentsjs@npm:2.7.0"
|
||||
checksum: df60a5faf79d85eba334a42f7bffb101f0e01a8ee8cc3705f17c67c70247415c4440ad6e8fdd3c74dc5be097a436c4e4831824bef039a12b69f05e84a07187b7
|
||||
"@webcomponents/webcomponentsjs@npm:2.8.0":
|
||||
version: 2.8.0
|
||||
resolution: "@webcomponents/webcomponentsjs@npm:2.8.0"
|
||||
checksum: 186373c0308a35abf4e97228b1cc5a5dc5c694701ccef2046baa1598636b08be5ec16baff2770584fef1aa241837fc36316f0b415396202eb444b36aa1a01117
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -9535,8 +9535,8 @@ __metadata:
|
||||
"@vue/web-component-wrapper": 1.3.0
|
||||
"@web/dev-server": 0.1.37
|
||||
"@web/dev-server-rollup": 0.4.0
|
||||
"@webcomponents/scoped-custom-element-registry": 0.0.8
|
||||
"@webcomponents/webcomponentsjs": 2.7.0
|
||||
"@webcomponents/scoped-custom-element-registry": 0.0.9
|
||||
"@webcomponents/webcomponentsjs": 2.8.0
|
||||
app-datepicker: 5.1.1
|
||||
babel-loader: 9.1.2
|
||||
babel-plugin-template-html-minifier: 4.1.0
|
||||
@ -9633,7 +9633,7 @@ __metadata:
|
||||
vue2-daterange-picker: 0.6.8
|
||||
webpack: =5.72.1
|
||||
webpack-cli: 5.0.1
|
||||
webpack-dev-server: 4.13.1
|
||||
webpack-dev-server: 4.13.2
|
||||
webpack-manifest-plugin: 5.0.0
|
||||
webpackbar: 5.0.2
|
||||
weekstart: 2.0.0
|
||||
@ -15863,9 +15863,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"webpack-dev-server@npm:4.13.1":
|
||||
version: 4.13.1
|
||||
resolution: "webpack-dev-server@npm:4.13.1"
|
||||
"webpack-dev-server@npm:4.13.2":
|
||||
version: 4.13.2
|
||||
resolution: "webpack-dev-server@npm:4.13.2"
|
||||
dependencies:
|
||||
"@types/bonjour": ^3.5.9
|
||||
"@types/connect-history-api-fallback": ^1.3.5
|
||||
@ -15906,7 +15906,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
webpack-dev-server: bin/webpack-dev-server.js
|
||||
checksum: f70611544b7d964a31eb3d934d7c2b376b97e6927a89e03b2e21cfa5812bb639625cd18fd350de1604ba6c455b324135523a894032f28c69d90d90682e4f3b7d
|
||||
checksum: 9bf573abf05b0e0f1e8219820f6264e25a0f8ee6aebed3c0d0449c24a37f88b575972e0a2bec426112ee37d48c8f5090e7754aa1873206d3c9b6344a54718232
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user