Compare commits

..

1 Commits

Author SHA1 Message Date
Bram Kragten
a548d13931 Allow partial open of sidebar 2023-04-01 18:46:52 +02:00
201 changed files with 3650 additions and 5894 deletions

View File

@@ -22,7 +22,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
with:
ref: dev
@@ -58,7 +58,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
with:
ref: master

View File

@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.6.0
with:
@@ -48,7 +48,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.6.0
with:
@@ -66,7 +66,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.6.0
with:
@@ -84,7 +84,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.6.0
with:

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View File

@@ -17,13 +17,13 @@ jobs:
deploy_dev:
runs-on: ubuntu-latest
name: Demo Development
if: github.event_name != 'push' || github.ref_name != 'master'
if: github.event_name != 'push' || github.ref != 'master'
environment:
name: Demo Development
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
with:
ref: dev
@@ -53,13 +53,13 @@ jobs:
deploy_master:
runs-on: ubuntu-latest
name: Demo Production
if: github.event_name == 'push' && github.ref_name == 'master'
if: github.event_name == 'push' && github.ref == 'master'
environment:
name: Demo Production
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
with:
ref: master

View File

@@ -17,7 +17,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.6.0

View File

@@ -22,7 +22,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
- name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.6.0

View File

@@ -21,7 +21,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4

View File

@@ -24,7 +24,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
- name: Upload Translations
run: |

View File

@@ -89,7 +89,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
"@babel/preset-env",
{
useBuiltIns: "entry",
corejs: { version: "3.30", proposals: true },
corejs: { version: "3.29", proposals: true },
bugfixes: true,
},
],

View File

@@ -8,7 +8,6 @@ const gulp = require("gulp");
const jszip = require("jszip");
const tar = require("tar");
const { Octokit } = require("@octokit/rest");
const { retry } = require("@octokit/plugin-retry");
const { createOAuthDeviceAuth } = require("@octokit/auth-oauth-device");
const MAX_AGE = 24; // hours
@@ -96,7 +95,7 @@ gulp.task("fetch-nightly-translations", async function () {
// Authenticate with token and request workflow runs from GitHub
console.log("Fetching new translations...");
const octokit = new (Octokit.plugin(retry))({
const octokit = new Octokit({
userAgent: "Fetch Nightly Translations",
auth: tokenAuth.token,
});

View File

@@ -3,7 +3,7 @@ const gulp = require("gulp");
const fs = require("fs");
const path = require("path");
const { marked } = require("marked");
const { glob } = require("glob");
const glob = require("glob");
const yaml = require("js-yaml");
const env = require("../env.cjs");

View File

@@ -92,7 +92,11 @@ export class HassioAddonStore extends LitElement {
.route=${this.route}
.header=${this.supervisor.localize("panel.store")}
>
<ha-button-menu slot="toolbar-icon" @action=${this._handleAction}>
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._handleAction}
>
<ha-icon-button
.label=${this.supervisor.localize("common.menu")}
.path=${mdiDotsVertical}
@@ -216,7 +220,7 @@ export class HassioAddonStore extends LitElement {
});
}
private _filterChanged(e) {
private async _filterChanged(e) {
this._filter = e.detail.value;
}

View File

@@ -168,7 +168,7 @@ class HassioAddonConfig extends LitElement {
${this.supervisor.localize("addon.configuration.options.header")}
</h2>
<div class="card-menu">
<ha-button-menu @action=${this._handleAction}>
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<ha-icon-button
.label=${this.supervisor.localize("common.menu")}
.path=${mdiDotsVertical}

View File

@@ -29,6 +29,7 @@ import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../../src/common/config/version";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { navigate } from "../../../../src/common/navigate";
import "../../../../src/components/buttons/ha-call-api-button";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card";
@@ -46,7 +47,6 @@ import {
HassioAddonSetOptionParams,
HassioAddonSetSecurityParams,
installHassioAddon,
rebuildLocalAddon,
restartHassioAddon,
setHassioAddonOption,
setHassioAddonSecurity,
@@ -640,12 +640,13 @@ class HassioAddonInfo extends LitElement {
</ha-progress-button>
${this.addon.build
? html`
<ha-progress-button
<ha-call-api-button
class="warning"
@click=${this._rebuildClicked}
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/rebuild"
>
${this.supervisor.localize("addon.dashboard.rebuild")}
</ha-progress-button>
</ha-call-api-button>
`
: ""}`
: ""}
@@ -965,21 +966,6 @@ class HassioAddonInfo extends LitElement {
button.progress = false;
}
private async _rebuildClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
try {
await rebuildLocalAddon(this.hass, this.addon.slug);
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("addon.dashboard.action_error.rebuild"),
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _startClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
@@ -1138,6 +1124,10 @@ class HassioAddonInfo extends LitElement {
ha-svg-icon.stopped {
color: var(--error-color);
}
ha-call-api-button {
font-weight: 500;
color: var(--primary-color);
}
protection-enable mwc-button {
--mdc-theme-primary: white;
}

View File

@@ -195,7 +195,11 @@ export class HassioBackups extends LitElement {
: "/config"}
supervisor
>
<ha-button-menu slot="toolbar-icon" @action=${this._handleAction}>
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._handleAction}
>
<ha-icon-button
.label=${this.supervisor?.localize("common.menu")}
.path=${mdiDotsVertical}

View File

@@ -316,7 +316,7 @@ export class DialogHassioNetwork
>
<div class="radio-row">
<ha-formfield
.label=${this.supervisor.localize("dialog.network.auto")}
.label=${this.supervisor.localize("dialog.network.dhcp")}
>
<ha-radio
@change=${this._handleRadioValueChanged}

View File

@@ -44,6 +44,10 @@ export const hassioStyle = css`
grid-template-columns: repeat(auto-fit, minmax(300px, 0.25fr));
}
}
ha-call-api-button {
font-weight: 500;
color: var(--primary-color);
}
.error {
color: var(--error-color);
margin-top: 16px;

View File

@@ -184,7 +184,7 @@ class HassioHostInfo extends LitElement {
`
: ""}
<ha-button-menu>
<ha-button-menu corner="BOTTOM_START">
<ha-icon-button
.label=${this.supervisor.localize("common.menu")}
.path=${mdiDotsVertical}

View File

@@ -26,13 +26,13 @@
"type": "module",
"dependencies": {
"@braintree/sanitize-url": "6.0.2",
"@codemirror/autocomplete": "6.5.1",
"@codemirror/autocomplete": "6.4.2",
"@codemirror/commands": "6.2.2",
"@codemirror/language": "6.6.0",
"@codemirror/legacy-modes": "6.3.2",
"@codemirror/search": "6.3.0",
"@codemirror/state": "6.2.0",
"@codemirror/view": "6.9.4",
"@codemirror/view": "6.9.3",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.5.1",
"@formatjs/intl-getcanonicallocales": "2.1.0",
@@ -90,18 +90,18 @@
"@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "23.3.10",
"@vaadin/vaadin-themable-mixin": "23.3.10",
"@vaadin/combo-box": "23.3.9",
"@vaadin/vaadin-themable-mixin": "23.3.9",
"@vibrant/color": "3.2.1-alpha.1",
"@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.9",
"@webcomponents/webcomponentsjs": "2.8.0",
"@webcomponents/scoped-custom-element-registry": "0.0.8",
"@webcomponents/webcomponentsjs": "2.7.0",
"app-datepicker": "5.1.1",
"chart.js": "3.3.2",
"comlink": "4.4.1",
"core-js": "3.30.1",
"core-js": "3.29.1",
"cropperjs": "1.5.13",
"date-fns": "2.29.3",
"date-fns-tz": "2.0.0",
@@ -116,7 +116,7 @@
"js-yaml": "4.1.0",
"leaflet": "1.9.3",
"leaflet-draw": "1.0.4",
"lit": "2.7.2",
"lit": "2.7.0",
"marked": "4.3.0",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
@@ -148,7 +148,7 @@
"xss": "1.0.14"
},
"devDependencies": {
"@babel/core": "7.21.4",
"@babel/core": "7.21.3",
"@babel/plugin-external-helpers": "7.18.6",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-decorators": "7.21.0",
@@ -158,17 +158,16 @@
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/plugin-syntax-import-meta": "7.10.4",
"@babel/plugin-syntax-top-level-await": "7.14.5",
"@babel/preset-env": "7.21.4",
"@babel/preset-typescript": "7.21.4",
"@babel/preset-env": "7.20.2",
"@babel/preset-typescript": "7.21.0",
"@koa/cors": "4.0.0",
"@octokit/auth-oauth-device": "4.0.4",
"@octokit/plugin-retry": "4.1.3",
"@octokit/rest": "19.0.7",
"@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.3",
"@rollup/plugin-commonjs": "24.1.0",
"@rollup/plugin-commonjs": "24.0.1",
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-node-resolve": "15.0.2",
"@rollup/plugin-node-resolve": "15.0.1",
"@rollup/plugin-replace": "5.0.2",
"@types/chromecast-caf-receiver": "5.0.12",
"@types/chromecast-caf-sender": "1.0.5",
@@ -185,15 +184,15 @@
"@types/sortablejs": "1.15.1",
"@types/tar": "6.1.4",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.58.0",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.57.0",
"@web/dev-server": "0.1.37",
"@web/dev-server-rollup": "0.4.0",
"babel-loader": "9.1.2",
"babel-plugin-template-html-minifier": "4.1.0",
"chai": "4.3.7",
"del": "7.0.0",
"eslint": "8.38.0",
"eslint": "8.37.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.0.0",
"eslint-config-prettier": "8.8.0",
@@ -201,24 +200,24 @@
"eslint-plugin-disable": "2.0.3",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-lit": "1.8.2",
"eslint-plugin-lit-a11y": "2.4.1",
"eslint-plugin-lit-a11y": "2.4.0",
"eslint-plugin-unused-imports": "2.0.0",
"eslint-plugin-wc": "1.4.0",
"esprima": "4.0.1",
"fancy-log": "2.0.0",
"fs-extra": "11.1.1",
"glob": "10.1.0",
"glob": "9.3.2",
"gulp": "4.0.2",
"gulp-flatmap": "1.0.2",
"gulp-json-transform": "0.4.8",
"gulp-merge-json": "2.1.2",
"gulp-rename": "2.0.0",
"gulp-zopfli-green": "6.0.1",
"html-minifier-terser": "7.2.0",
"html-minifier-terser": "7.1.0",
"husky": "8.0.3",
"instant-mocha": "1.5.1",
"instant-mocha": "1.5.0",
"jszip": "3.10.1",
"lint-staged": "13.2.1",
"lint-staged": "13.2.0",
"lit-analyzer": "1.2.1",
"lodash.template": "4.5.0",
"magic-string": "0.30.0",
@@ -245,7 +244,7 @@
"vinyl-source-stream": "2.0.0",
"webpack": "=5.72.1",
"webpack-cli": "5.0.1",
"webpack-dev-server": "4.13.3",
"webpack-dev-server": "4.13.1",
"webpack-manifest-plugin": "5.0.0",
"webpackbar": "5.0.2",
"workbox-build": "6.5.4"

View File

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

View File

@@ -9,7 +9,6 @@ import {
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-alert";
import "../components/ha-checkbox";
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
@@ -21,12 +20,13 @@ import {
DataEntryFlowStep,
DataEntryFlowStepForm,
} from "../data/data_entry_flow";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import "./ha-password-manager-polyfill";
type State = "loading" | "error" | "step";
@customElement("ha-auth-flow")
export class HaAuthFlow extends LitElement {
export class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
@property({ attribute: false }) public authProvider?: AuthProvider;
@property() public clientId?: string;
@@ -35,8 +35,6 @@ export class HaAuthFlow extends LitElement {
@property() public oauth2State?: string;
@property() public localize!: LocalizeFunc;
@state() private _state: State = "loading";
@state() private _stepData?: Record<string, any>;

View File

@@ -82,13 +82,12 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
.redirectUri=${this.redirectUri}
.oauth2State=${this.oauth2State}
.authProvider=${this._authProvider}
.localize=${this.localize}
></ha-auth-flow>
${inactiveProviders.length > 0
? html`
<ha-pick-auth-provider
.localize=${this.localize}
.resources=${this.resources}
.clientId=${this.clientId}
.authProviders=${inactiveProviders}
@pick-auth-provider=${this._handleAuthProviderPick}

View File

@@ -2,10 +2,10 @@ import "@material/mwc-list";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-icon-next";
import "../components/ha-list-item";
import { AuthProvider } from "../data/auth";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
declare global {
interface HASSDomEvents {
@@ -14,11 +14,9 @@ declare global {
}
@customElement("ha-pick-auth-provider")
export class HaPickAuthProvider extends LitElement {
export class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) {
@property() public authProviders: AuthProvider[] = [];
@property() public localize!: LocalizeFunc;
protected render() {
return html`
<p>${this.localize("ui.panel.page-authorize.pick_auth_provider")}:</p>

View File

@@ -24,47 +24,26 @@ import { LocalizeFunc } from "../translations/localize";
import { computeDomain } from "./compute_domain";
import { supportsFeatureFromAttributes } from "./supports-feature";
export const computeStateDisplaySingleEntity = (
localize: LocalizeFunc,
stateObj: HassEntity,
locale: FrontendLocaleData,
entity: EntityRegistryDisplayEntry | undefined,
state?: string
): string =>
computeStateDisplayFromEntityAttributes(
localize,
locale,
entity,
stateObj.entity_id,
stateObj.attributes,
state !== undefined ? state : stateObj.state
);
export const computeStateDisplay = (
localize: LocalizeFunc,
stateObj: HassEntity,
locale: FrontendLocaleData,
entities: HomeAssistant["entities"],
state?: string
): string => {
const entity = entities[stateObj.entity_id] as
| EntityRegistryDisplayEntry
| undefined;
return computeStateDisplayFromEntityAttributes(
): string =>
computeStateDisplayFromEntityAttributes(
localize,
locale,
entity,
entities,
stateObj.entity_id,
stateObj.attributes,
state !== undefined ? state : stateObj.state
);
};
export const computeStateDisplayFromEntityAttributes = (
localize: LocalizeFunc,
locale: FrontendLocaleData,
entity: EntityRegistryDisplayEntry | undefined,
entities: HomeAssistant["entities"],
entityId: string,
attributes: any,
state: string
@@ -73,6 +52,8 @@ export const computeStateDisplayFromEntityAttributes = (
return localize(`state.default.${state}`);
}
const entity = entities[entityId] as EntityRegistryDisplayEntry | undefined;
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
if (isNumericFromAttributes(attributes)) {
// state is duration

View File

@@ -0,0 +1,17 @@
import { refine, string } from "superstruct";
const isEntityId = (value: string): boolean => value.includes(".");
export const entityId = () =>
refine(string(), "entity ID (domain.entity)", isEntityId);
const isEntityIdOrAll = (value: string): boolean => {
if (value === "all") {
return true;
}
return isEntityId(value);
};
export const entityIdOrAll = () =>
refine(string(), "entity ID (domain.entity or all)", isEntityIdOrAll);

View File

@@ -4,7 +4,7 @@ import { FrontendLocaleData } from "../../data/translation";
export const blankBeforePercent = (
localeOptions: FrontendLocaleData
): string => {
switch (localeOptions?.language) {
switch (localeOptions.language) {
case "cz":
case "de":
case "fi":

View File

@@ -0,0 +1,77 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import "./ha-progress-button";
@customElement("ha-call-api-button")
class HaCallApiButton extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public method: "POST" | "GET" | "PUT" | "DELETE" = "POST";
@property() public data = {};
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public progress = false;
@property() public path?: string;
@query("ha-progress-button", true) private _progressButton;
render() {
return html`
<ha-progress-button
.progress=${this.progress}
@click=${this._buttonTapped}
?disabled=${this.disabled}
><slot></slot
></ha-progress-button>
`;
}
async _buttonTapped() {
this.progress = true;
const eventData: {
method: string;
path: string;
data: any;
success?: boolean;
response?: any;
} = {
method: this.method,
path: this.path!,
data: this.data,
};
try {
const resp = await this.hass.callApi(this.method, this.path!, this.data);
this.progress = false;
this._progressButton.actionSuccess();
eventData.success = true;
eventData.response = resp;
} catch (err: any) {
this.progress = false;
this._progressButton.actionError();
eventData.success = false;
eventData.response = err;
}
fireEvent(this, "hass-api-called", eventData as any);
}
static get styles(): CSSResultGroup {
return css`
:host([disabled]) {
pointer-events: none;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-call-api-button": HaCallApiButton;
}
}

View File

@@ -276,11 +276,7 @@ export default class HaChartBase extends LitElement {
top: this.chart!.canvas.offsetTop + context.tooltip.caretY + 12 + "px",
left:
this.chart!.canvas.offsetLeft +
clamp(
context.tooltip.caretX,
100,
this.clientWidth - 100 - this.paddingYAxis
) -
clamp(context.tooltip.caretX, 100, this.clientWidth - 100) -
100 +
"px",
};
@@ -306,7 +302,7 @@ export default class HaChartBase extends LitElement {
return css`
:host {
display: block;
position: var(--chart-base-position, relative);
position: relative;
}
.chartContainer {
overflow: hidden;

View File

@@ -73,7 +73,7 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
main?: boolean;
title: TemplateResult | string;
label?: TemplateResult | string;
type?: "numeric" | "icon" | "icon-button" | "overflow-menu" | "flex";
type?: "numeric" | "icon" | "icon-button" | "overflow-menu";
template?: (data: any, row: T) => TemplateResult | string | typeof nothing;
width?: string;
maxWidth?: string;
@@ -359,10 +359,10 @@ export class HaDataTable extends LitElement {
return nothing;
}
if (row.append) {
return html`<div class="mdc-data-table__row">${row.content}</div>`;
return html` <div class="mdc-data-table__row">${row.content}</div> `;
}
if (row.empty) {
return html`<div class="mdc-data-table__row"></div>`;
return html` <div class="mdc-data-table__row"></div> `;
}
return html`
<div
@@ -406,7 +406,6 @@ export class HaDataTable extends LitElement {
<div
role=${column.main ? "rowheader" : "cell"}
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--flex": column.type === "flex",
"mdc-data-table__cell--numeric": column.type === "numeric",
"mdc-data-table__cell--icon": column.type === "icon",
"mdc-data-table__cell--icon-button":
@@ -664,10 +663,6 @@ export class HaDataTable extends LitElement {
box-sizing: border-box;
}
.mdc-data-table__cell.mdc-data-table__cell--flex {
display: flex;
}
.mdc-data-table__cell.mdc-data-table__cell--icon {
overflow: initial;
}
@@ -984,7 +979,6 @@ export class HaDataTable extends LitElement {
}
lit-virtualizer {
contain: size layout !important;
overscroll-behavior: contain;
}
`,
];

View File

@@ -1,129 +0,0 @@
import "@material/mwc-button/mwc-button";
import { mdiDeleteOutline, mdiPlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import "./ha-area-picker";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-aliases-editor")
class AliasesEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public aliases!: string[];
protected render() {
if (!this.aliases) {
return nothing;
}
return html`
${this.aliases.map(
(alias, index) => html`
<div class="layout horizontal center-center row">
<ha-textfield
dialogInitialFocus=${index}
.index=${index}
class="flex-auto"
.label=${this.hass!.localize("ui.dialogs.aliases.input_label", {
number: index + 1,
})}
.value=${alias}
?data-last=${index === this.aliases.length - 1}
@input=${this._editAlias}
@keydown=${this._keyDownAlias}
></ha-textfield>
<ha-icon-button
.index=${index}
slot="navigationIcon"
label=${this.hass!.localize("ui.dialogs.aliases.remove_alias", {
number: index + 1,
})}
@click=${this._removeAlias}
.path=${mdiDeleteOutline}
></ha-icon-button>
</div>
`
)}
<div class="layout horizontal center-center">
<mwc-button @click=${this._addAlias}>
${this.hass!.localize("ui.dialogs.aliases.add_alias")}
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-button>
</div>
`;
}
private async _addAlias() {
this.aliases = [...this.aliases, ""];
this._fireChanged(this.aliases);
await this.updateComplete;
const field = this.shadowRoot?.querySelector(`ha-textfield[data-last]`) as
| HaTextField
| undefined;
field?.focus();
}
private async _editAlias(ev: Event) {
const index = (ev.target as any).index;
const aliases = [...this.aliases];
aliases[index] = (ev.target as any).value;
this._fireChanged(aliases);
}
private async _keyDownAlias(ev: KeyboardEvent) {
if (ev.key === "Enter") {
ev.stopPropagation();
this._addAlias();
}
}
private async _removeAlias(ev: Event) {
const index = (ev.target as any).index;
const aliases = [...this.aliases];
aliases.splice(index, 1);
this._fireChanged(aliases);
}
private _fireChanged(value) {
fireEvent(this, "value-changed", { value });
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.row {
margin-bottom: 8px;
}
ha-textfield {
display: block;
}
ha-icon-button {
display: block;
}
mwc-button {
margin-left: 8px;
}
#alias_input {
margin-top: 8px;
}
.alias {
border: 1px solid var(--divider-color);
border-radius: 4px;
margin-top: 4px;
--mdc-icon-button-size: 24px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-aliases-editor": AliasesEditor;
}
}

View File

@@ -2,9 +2,9 @@ import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { LocalizeFunc } from "../common/translations/localize";
import type { Analytics, AnalyticsPreferences } from "../data/analytics";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-settings-row";
import "./ha-switch";
import type { HaSwitch } from "./ha-switch";
@@ -19,7 +19,7 @@ declare global {
@customElement("ha-analytics")
export class HaAnalytics extends LitElement {
@property({ attribute: false }) public localize!: LocalizeFunc;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public analytics?: Analytics;
@@ -34,12 +34,12 @@ export class HaAnalytics extends LitElement {
return html`
<ha-settings-row>
<span slot="heading" data-for="base">
${this.localize(
${this.hass.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.title`
)}
</span>
<span slot="description" data-for="base">
${this.localize(
${this.hass.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.description`
)}
</span>
@@ -57,12 +57,12 @@ export class HaAnalytics extends LitElement {
html`
<ha-settings-row>
<span slot="heading" data-for=${preference}>
${this.localize(
${this.hass.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.title`
)}
</span>
<span slot="description" data-for=${preference}>
${this.localize(
${this.hass.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.description`
)}
</span>
@@ -77,7 +77,7 @@ export class HaAnalytics extends LitElement {
${!baseEnabled
? html`
<simple-tooltip animation-delay="0" position="right">
${this.localize(
${this.hass.localize(
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
)}
</simple-tooltip>
@@ -89,12 +89,12 @@ export class HaAnalytics extends LitElement {
)}
<ha-settings-row>
<span slot="heading" data-for="diagnostics">
${this.localize(
${this.hass.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.title`
)}
</span>
<span slot="description" data-for="diagnostics">
${this.localize(
${this.hass.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.description`
)}
</span>

View File

@@ -10,7 +10,7 @@ import type { HaIconButton } from "./ha-icon-button";
export class HaButtonMenu extends LitElement {
protected readonly [FOCUS_TARGET];
@property() public corner: Corner = "BOTTOM_START";
@property() public corner: Corner = "TOP_START";
@property() public menuCorner: MenuCorner = "START";

View File

@@ -35,7 +35,7 @@ interface FilterValue {
export class HaRelatedFilterButtonMenu extends LitElement {
@property() public hass!: HomeAssistant;
@property() public corner: Corner = "BOTTOM_START";
@property() public corner: Corner = "TOP_START";
@property({ type: Boolean, reflect: true }) public narrow = false;

View File

@@ -310,8 +310,6 @@ export class HaControlSelect extends LitElement {
.option .content span {
display: block;
width: 100%;
-webkit-hyphens: auto;
-moz-hyphens: auto;
hyphens: auto;
}
:host([vertical]) {

View File

@@ -1,106 +0,0 @@
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValueMap,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { Agent, listAgents } from "../data/conversation";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
const NONE = "__NONE_OPTION__";
@customElement("ha-conversation-agent-picker")
export class HaConversationAgentPicker extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = false;
@state() _agents?: Agent[];
@state() _defaultAgent: string | null = null;
protected render() {
if (!this._agents) {
return nothing;
}
const value = this.value ?? (this.required ? this._defaultAgent : NONE);
return html`
<ha-select
.label=${this.label ||
this.hass!.localize(
"ui.components.coversation-agent-picker.conversation_agent"
)}
.value=${value}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize(
"ui.components.coversation-agent-picker.none"
)}
</ha-list-item>`
: nothing}
${this._agents.map(
(agent) =>
html`<ha-list-item .value=${agent.id}>${agent.name}</ha-list-item>`
)}
</ha-select>
`;
}
protected firstUpdated(
changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
): void {
super.firstUpdated(changedProperties);
listAgents(this.hass).then((agents) => {
this._agents = agents.agents;
this._defaultAgent = agents.default_agent;
});
}
static get styles(): CSSResultGroup {
return css`
ha-select {
width: 100%;
}
`;
}
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
) {
return;
}
this.value = target.value === NONE ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-conversation-agent-picker": HaConversationAgentPicker;
}
}

View File

@@ -1,83 +1,18 @@
import { DrawerBase } from "@material/mwc-drawer/mwc-drawer-base";
import { styles } from "@material/mwc-drawer/mwc-drawer.css";
import { css, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
const blockingElements = (document as any).$blockingElements;
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-drawer")
export class HaDrawer extends DrawerBase {
@property() public direction: "ltr" | "rtl" = "ltr";
private _mc?: HammerManager;
protected createAdapter() {
return {
...super.createAdapter(),
trapFocus: () => {
blockingElements.push(this);
this.appContent.inert = true;
document.body.style.overflow = "hidden";
},
releaseFocus: () => {
blockingElements.remove(this);
this.appContent.inert = false;
document.body.style.overflow = "";
},
};
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("direction")) {
this.mdcRoot.dir = this.direction;
}
if (changedProps.has("open") && this.open && this.type === "modal") {
this._setupSwipe();
} else if (this._mc) {
this._mc.destroy();
this._mc = undefined;
}
}
private async _setupSwipe() {
const hammer = await import("../resources/hammer");
this._mc = new hammer.Manager(document, {
touchAction: "pan-y",
});
this._mc.add(
new hammer.Swipe({
direction:
this.direction === "rtl"
? hammer.DIRECTION_RIGHT
: hammer.DIRECTION_LEFT,
})
);
this._mc.on("swipeleft swiperight", () => {
fireEvent(this, "hass-toggle-menu", { open: false });
});
}
static override styles = [
styles,
css`
.mdc-drawer {
position: fixed;
top: 0;
}
.mdc-drawer.mdc-drawer--modal.mdc-drawer--open {
z-index: 200;
}
.mdc-drawer-app-content {
overflow: unset;
flex: none;
padding-left: var(--mdc-drawer-width);
padding-inline-start: var(--mdc-drawer-width);
padding-inline-end: initial;
direction: var(--direction);
width: 100%;
box-sizing: border-box;
.mdc-drawer--modal.mdc-drawer--open {
left: min(0px, var(--drawer-modal-left-offset));
}
`,
];

View File

@@ -71,7 +71,6 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
display: block;
--expansion-panel-content-padding: 0;
border-radius: 6px;
--ha-card-border-radius: 6px;
}
ha-svg-icon,
ha-icon {

View File

@@ -82,6 +82,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
<ha-button-menu
.disabled=${this.disabled}
fixed
corner="BOTTOM_START"
@opened=${this._handleOpen}
@closed=${this._handleClose}
multi

View File

@@ -38,6 +38,7 @@ export class HaIconOverflowMenu extends LitElement {
@click=${this._handleIconOverflowMenuOpened}
@closed=${this._handleIconOverflowMenuClosed}
class="ha-icon-overflow-menu-overflow"
corner="BOTTOM_START"
absolute
>
<ha-icon-button

View File

@@ -30,9 +30,6 @@ export class HaListItem extends ListItemBase {
margin-inline-end: 0px !important;
direction: var(--direction);
}
.mdc-deprecated-list-item__meta {
display: var(--mdc-list-item-meta-display);
}
:host([multiline-secondary]) {
height: auto;
}
@@ -57,9 +54,6 @@ export class HaListItem extends ListItemBase {
.mdc-deprecated-list-item__primary-text::before {
display: none;
}
:host([disabled]) {
color: var(--disabled-text-color);
}
`,
];
}

View File

@@ -70,7 +70,11 @@ class HaQrScanner extends LitElement {
? html`<video></video>
<div id="canvas-container">
${this._cameras && this._cameras.length > 1
? html`<ha-button-menu fixed @closed=${stopPropagation}>
? html`<ha-button-menu
corner="BOTTOM_START"
fixed
@closed=${stopPropagation}
>
<ha-icon-button
slot="trigger"
.label=${this.localize(

View File

@@ -1,45 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { ConversationAgentSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-conversation-agent-picker";
@customElement("ha-selector-conversation_agent")
export class HaConversationAgentSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: ConversationAgentSelector;
@property() public value?: any;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`<ha-conversation-agent-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
></ha-conversation-agent-picker>`;
}
static styles = css`
ha-conversation-agent-picker {
width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-conversation_agent": HaConversationAgentSelector;
}
}

View File

@@ -135,7 +135,7 @@ export class HaSelectSelector extends LitElement {
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.value=${""}
.value=${this._filter}
.items=${optionItems}
.allowCustomValue=${this.selector.select.custom_value ?? false}
@filter-changed=${this._filterChanged}
@@ -213,7 +213,7 @@ export class HaSelectSelector extends LitElement {
private _valueChanged(ev) {
ev.stopPropagation();
const value = ev.detail?.value || ev.target.value;
if (this.disabled || value === undefined || value === this.value) {
if (this.disabled || value === undefined) {
return;
}
fireEvent(this, "value-changed", {

View File

@@ -1,50 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { STTSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-stt-picker";
@customElement("ha-selector-stt")
export class HaSTTSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: STTSelector;
@property() public value?: any;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@property({ attribute: false }) public context?: {
language?: string;
};
protected render() {
return html`<ha-stt-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.language=${this.selector.stt?.language || this.context?.language}
.disabled=${this.disabled}
.required=${this.required}
></ha-stt-picker>`;
}
static styles = css`
ha-stt-picker {
width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-stt": HaSTTSelector;
}
}

View File

@@ -1,50 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { TTSSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-tts-picker";
@customElement("ha-selector-tts")
export class HaTTSSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: TTSSelector;
@property() public value?: any;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@property({ attribute: false }) public context?: {
language?: string;
};
protected render() {
return html`<ha-tts-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.language=${this.selector.tts?.language || this.context?.language}
.disabled=${this.disabled}
.required=${this.required}
></ha-tts-picker>`;
}
static styles = css`
ha-tts-picker {
width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-tts": HaTTSSelector;
}
}

View File

@@ -6,7 +6,7 @@ import { HomeAssistant } from "../../types";
import "../../panels/lovelace/components/hui-action-editor";
import { ActionConfig } from "../../data/lovelace";
@customElement("ha-selector-ui_action")
@customElement("ha-selector-ui-action")
export class HaSelectorUiAction extends LitElement {
@property() public hass!: HomeAssistant;
@@ -24,7 +24,7 @@ export class HaSelectorUiAction extends LitElement {
.label=${this.label}
.hass=${this.hass}
.config=${this.value}
.actions=${this.selector.ui_action?.actions}
.actions=${this.selector["ui-action"]?.actions}
.tooltipText=${this.helper}
@value-changed=${this._valueChanged}
></hui-action-editor>

View File

@@ -6,7 +6,7 @@ import { UiColorSelector } from "../../data/selector";
import "../../panels/lovelace/components/hui-color-picker";
import { HomeAssistant } from "../../types";
@customElement("ha-selector-ui_color")
@customElement("ha-selector-ui-color")
export class HaSelectorUiColor extends LitElement {
@property() public hass!: HomeAssistant;

View File

@@ -17,7 +17,6 @@ const LOAD_ELEMENTS = {
boolean: () => import("./ha-selector-boolean"),
color_rgb: () => import("./ha-selector-color-rgb"),
config_entry: () => import("./ha-selector-config-entry"),
conversation_agent: () => import("./ha-selector-conversation-agent"),
constant: () => import("./ha-selector-constant"),
date: () => import("./ha-selector-date"),
datetime: () => import("./ha-selector-datetime"),
@@ -31,7 +30,6 @@ const LOAD_ELEMENTS = {
object: () => import("./ha-selector-object"),
select: () => import("./ha-selector-select"),
state: () => import("./ha-selector-state"),
stt: () => import("./ha-selector-stt"),
target: () => import("./ha-selector-target"),
template: () => import("./ha-selector-template"),
text: () => import("./ha-selector-text"),
@@ -39,15 +37,12 @@ const LOAD_ELEMENTS = {
icon: () => import("./ha-selector-icon"),
media: () => import("./ha-selector-media"),
theme: () => import("./ha-selector-theme"),
tts: () => import("./ha-selector-tts"),
location: () => import("./ha-selector-location"),
color_temp: () => import("./ha-selector-color-temp"),
ui_action: () => import("./ha-selector-ui-action"),
ui_color: () => import("./ha-selector-ui-color"),
"ui-action": () => import("./ha-selector-ui-action"),
"ui-color": () => import("./ha-selector-ui-color"),
};
const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]);
@customElement("ha-selector")
export class HaSelector extends LitElement {
@property() public hass!: HomeAssistant;
@@ -77,11 +72,7 @@ export class HaSelector extends LitElement {
}
private get _type() {
const type = Object.keys(this.selector)[0];
if (LEGACY_UI_SELECTORS.has(type)) {
return type.replace("-", "_");
}
return type;
return Object.keys(this.selector)[0];
}
protected willUpdate(changedProps: PropertyValues) {
@@ -97,10 +88,6 @@ export class HaSelector extends LitElement {
if ("device" in selector) {
return handleLegacyDeviceSelector(selector);
}
const type = Object.keys(this.selector)[0];
if (LEGACY_UI_SELECTORS.has(type)) {
return { [type.replace("-", "_")]: selector[type] };
}
return selector;
});

View File

@@ -28,8 +28,8 @@ import {
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
nothing,
} from "lit";
import { customElement, eventOptions, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";

View File

@@ -1,126 +0,0 @@
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { computeStateName } from "../common/entity/compute_state_name";
import { debounce } from "../common/util/debounce";
import { listSTTEngines, STTEngine } from "../data/stt";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
const NONE = "__NONE_OPTION__";
@customElement("ha-stt-picker")
export class HaSTTPicker extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property() public language?: string;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = false;
@state() _engines: STTEngine[] = [];
protected render(): TemplateResult {
const value =
this.value ??
(this.required
? this._engines.find((engine) => engine.language_supported)
: NONE);
return html`
<ha-select
.label=${this.label ||
this.hass!.localize("ui.components.stt-picker.stt")}
.value=${value}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize("ui.components.stt-picker.none")}
</ha-list-item>`
: nothing}
${this._engines.map((engine) => {
const stateObj = this.hass!.states[engine.engine_id];
return html`<ha-list-item
.value=${engine.engine_id}
.disabled=${engine.language_supported === false}
>
${stateObj ? computeStateName(stateObj) : engine.engine_id}
</ha-list-item>`;
})}
</ha-select>
`;
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (!this.hasUpdated) {
this._updateEngines();
} else if (changedProperties.has("language")) {
this._debouncedUpdateEngines();
}
}
private _debouncedUpdateEngines = debounce(() => this._updateEngines(), 500);
private async _updateEngines() {
this._engines = (await listSTTEngines(this.hass, this.language)).providers;
if (
this.value &&
!this._engines.find((engine) => engine.engine_id === this.value)
?.language_supported
) {
this.value = undefined;
fireEvent(this, "value-changed", { value: this.value });
}
}
static get styles(): CSSResultGroup {
return css`
ha-select {
width: 100%;
}
`;
}
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
) {
return;
}
this.value = target.value === NONE ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-stt-picker": HaSTTPicker;
}
}

View File

@@ -283,6 +283,7 @@ 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}

View File

@@ -1,3 +1,4 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";

View File

@@ -1,126 +0,0 @@
import { debounce } from "chart.js/helpers";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { computeStateName } from "../common/entity/compute_state_name";
import { listTTSEngines, TTSEngine } from "../data/tts";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
const NONE = "__NONE_OPTION__";
@customElement("ha-tts-picker")
export class HaTTSPicker extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property() public language?: string;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = false;
@state() _engines: TTSEngine[] = [];
protected render(): TemplateResult {
const value =
this.value ??
(this.required
? this._engines.find((engine) => engine.language_supported)
: NONE);
return html`
<ha-select
.label=${this.label ||
this.hass!.localize("ui.components.tts-picker.tts")}
.value=${value}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize("ui.components.tts-picker.none")}
</ha-list-item>`
: nothing}
${this._engines.map((engine) => {
const stateObj = this.hass!.states[engine.engine_id];
return html`<ha-list-item
.value=${engine.engine_id}
.disabled=${engine.language_supported === false}
>
${stateObj ? computeStateName(stateObj) : engine.engine_id}
</ha-list-item>`;
})}
</ha-select>
`;
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (!this.hasUpdated) {
this._updateEngines();
} else if (changedProperties.has("language")) {
this._debouncedUpdateEngines();
}
}
private _debouncedUpdateEngines = debounce(() => this._updateEngines(), 500);
private async _updateEngines() {
this._engines = (await listTTSEngines(this.hass, this.language)).providers;
if (
this.value &&
!this._engines.find((engine) => engine.engine_id === this.value)
?.language_supported
) {
this.value = undefined;
fireEvent(this, "value-changed", { value: this.value });
}
}
static get styles(): CSSResultGroup {
return css`
ha-select {
width: 100%;
}
`;
}
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
) {
return;
}
this.value = target.value === NONE ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-tts-picker": HaTTSPicker;
}
}

View File

@@ -53,46 +53,53 @@ export const callAlarmAction = (
};
export type AlarmMode =
| "armed_home"
| "armed_away"
| "armed_night"
| "armed_vacation"
| "armed_custom_bypass"
| "away"
| "home"
| "night"
| "vacation"
| "custom_bypass"
| "disarmed";
type AlarmConfig = {
service: string;
feature?: AlarmControlPanelEntityFeature;
state: string;
path: string;
};
export const ALARM_MODES: Record<AlarmMode, AlarmConfig> = {
armed_home: {
feature: AlarmControlPanelEntityFeature.ARM_HOME,
service: "alarm_arm_home",
path: mdiHome,
},
armed_away: {
away: {
feature: AlarmControlPanelEntityFeature.ARM_AWAY,
service: "alarm_arm_away",
state: "armed_away",
path: mdiLock,
},
armed_night: {
feature: AlarmControlPanelEntityFeature.ARM_NIGHT,
service: "alarm_arm_night",
path: mdiMoonWaningCrescent,
home: {
feature: AlarmControlPanelEntityFeature.ARM_HOME,
service: "alarm_arm_home",
state: "armed_home",
path: mdiHome,
},
armed_vacation: {
feature: AlarmControlPanelEntityFeature.ARM_VACATION,
service: "alarm_arm_vacation",
path: mdiAirplane,
},
armed_custom_bypass: {
custom_bypass: {
feature: AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS,
service: "alarm_arm_custom_bypass",
state: "armed_custom_bypass",
path: mdiShield,
},
night: {
feature: AlarmControlPanelEntityFeature.ARM_NIGHT,
service: "alarm_arm_night",
state: "armed_night",
path: mdiMoonWaningCrescent,
},
vacation: {
feature: AlarmControlPanelEntityFeature.ARM_VACATION,
service: "alarm_arm_vacation",
state: "armed_vacation",
path: mdiAirplane,
},
disarmed: {
service: "alarm_disarm",
state: "disarmed",
path: mdiShieldOff,
},
};

View File

@@ -1,300 +0,0 @@
import type { HomeAssistant } from "../types";
import type { ConversationResult } from "./conversation";
import type { ResolvedMediaSource } from "./media_source";
import type { SpeechMetadata } from "./stt";
export interface AssistPipeline {
id: string;
conversation_engine: string;
language: string;
name: string;
stt_engine: string;
tts_engine: string;
}
export interface AssistPipelineMutableParams {
conversation_engine: string;
language: string;
name: string;
stt_engine: string;
tts_engine: string;
}
export interface assistSessionListing {
pipeline_session_id: string;
timestamp: string;
}
interface PipelineEventBase {
timestamp: string;
}
interface PipelineRunStartEvent extends PipelineEventBase {
type: "run-start";
data: {
pipeline: string;
language: string;
runner_data: {
stt_binary_handler_id: number | null;
timeout: number;
};
};
}
interface PipelineRunEndEvent extends PipelineEventBase {
type: "run-end";
data: Record<string, never>;
}
interface PipelineErrorEvent extends PipelineEventBase {
type: "error";
data: {
code: string;
message: string;
};
}
interface PipelineSTTStartEvent extends PipelineEventBase {
type: "stt-start";
data: {
engine: string;
metadata: SpeechMetadata;
};
}
interface PipelineSTTEndEvent extends PipelineEventBase {
type: "stt-end";
data: {
stt_output: { text: string };
};
}
interface PipelineIntentStartEvent extends PipelineEventBase {
type: "intent-start";
data: {
engine: string;
intent_input: string;
};
}
interface PipelineIntentEndEvent extends PipelineEventBase {
type: "intent-end";
data: {
intent_output: ConversationResult;
};
}
interface PipelineTTSStartEvent extends PipelineEventBase {
type: "tts-start";
data: {
engine: string;
tts_input: string;
};
}
interface PipelineTTSEndEvent extends PipelineEventBase {
type: "tts-end";
data: {
tts_output: ResolvedMediaSource;
};
}
export type PipelineRunEvent =
| PipelineRunStartEvent
| PipelineRunEndEvent
| PipelineErrorEvent
| PipelineSTTStartEvent
| PipelineSTTEndEvent
| PipelineIntentStartEvent
| PipelineIntentEndEvent
| PipelineTTSStartEvent
| PipelineTTSEndEvent;
export type PipelineRunOptions = (
| {
start_stage: "intent" | "tts";
input: { text: string };
}
| {
start_stage: "stt";
input: { sample_rate: number };
}
) & {
end_stage: "stt" | "intent" | "tts";
pipeline?: string;
conversation_id?: string | null;
};
export interface PipelineRun {
init_options?: PipelineRunOptions;
events: PipelineRunEvent[];
stage: "ready" | "stt" | "intent" | "tts" | "done" | "error";
run: PipelineRunStartEvent["data"];
error?: PipelineErrorEvent["data"];
stt?: PipelineSTTStartEvent["data"] &
Partial<PipelineSTTEndEvent["data"]> & { done: boolean };
intent?: PipelineIntentStartEvent["data"] &
Partial<PipelineIntentEndEvent["data"]> & { done: boolean };
tts?: PipelineTTSStartEvent["data"] &
Partial<PipelineTTSEndEvent["data"]> & { done: boolean };
}
export const processEvent = (
run: PipelineRun | undefined,
event: PipelineRunEvent,
options?: PipelineRunOptions
): PipelineRun | undefined => {
if (event.type === "run-start") {
run = {
init_options: options,
stage: "ready",
run: event.data,
events: [event],
};
return run;
}
if (!run) {
// eslint-disable-next-line no-console
console.warn("Received unexpected event before receiving session", event);
return undefined;
}
if (event.type === "stt-start") {
run = {
...run,
stage: "stt",
stt: { ...event.data, done: false },
};
} else if (event.type === "stt-end") {
run = {
...run,
stt: { ...run.stt!, ...event.data, done: true },
};
} else if (event.type === "intent-start") {
run = {
...run,
stage: "intent",
intent: { ...event.data, done: false },
};
} else if (event.type === "intent-end") {
run = {
...run,
intent: { ...run.intent!, ...event.data, done: true },
};
} else if (event.type === "tts-start") {
run = {
...run,
stage: "tts",
tts: { ...event.data, done: false },
};
} else if (event.type === "tts-end") {
run = {
...run,
tts: { ...run.tts!, ...event.data, done: true },
};
} else if (event.type === "run-end") {
run = { ...run, stage: "done" };
} else if (event.type === "error") {
run = { ...run, stage: "error", error: event.data };
} else {
run = { ...run };
}
run.events = [...run.events, event];
return run;
};
export const runAssistPipeline = (
hass: HomeAssistant,
callback: (event: PipelineRun) => void,
options: PipelineRunOptions
) => {
let run: PipelineRun | undefined;
const unsubProm = hass.connection.subscribeMessage<PipelineRunEvent>(
(updateEvent) => {
run = processEvent(run, updateEvent, options);
if (updateEvent.type === "run-end" || updateEvent.type === "error") {
unsubProm.then((unsub) => unsub());
}
if (run) {
callback(run);
}
},
{
...options,
type: "assist_pipeline/run",
}
);
return unsubProm;
};
export const listAssistPipelineRuns = (
hass: HomeAssistant,
pipeline_id: string
) =>
hass.callWS<{
pipeline_sessions: assistSessionListing[];
}>({
type: "assist_pipeline/pipeline_debug/list",
pipeline_id,
});
export const getAssistPipelineRun = (
hass: HomeAssistant,
pipeline_id: string,
pipeline_session_id: string
) =>
hass.callWS<{
runs: {
events: PipelineRunEvent[];
}[];
}>({
type: "assist_pipeline/pipeline_debug/get",
pipeline_id,
pipeline_session_id,
});
export const fetchAssistPipelines = (hass: HomeAssistant) =>
hass.callWS<{
pipelines: AssistPipeline[];
preferred_pipeline: string | null;
}>({
type: "assist_pipeline/pipeline/list",
});
export const createAssistPipeline = (
hass: HomeAssistant,
pipeline: AssistPipelineMutableParams
) =>
hass.callWS<AssistPipeline>({
type: "assist_pipeline/pipeline/create",
...pipeline,
});
export const updateAssistPipeline = (
hass: HomeAssistant,
pipeline_id: string,
pipeline: Partial<AssistPipelineMutableParams>
) =>
hass.callWS<AssistPipeline>({
type: "assist_pipeline/pipeline/update",
pipeline_id,
...pipeline,
});
export const setAssistPipelinePreferred = (
hass: HomeAssistant,
pipeline_id: string
) =>
hass.callWS({
type: "assist_pipeline/pipeline/set_preferred",
pipeline_id,
});
export const deleteAssistPipeline = (hass: HomeAssistant, pipelineId: string) =>
hass.callWS<void>({
type: "assist_pipeline/pipeline/delete",
pipeline_id: pipelineId,
});

View File

@@ -123,8 +123,6 @@ export interface TimePatternTrigger extends BaseTrigger {
export interface WebhookTrigger extends BaseTrigger {
platform: "webhook";
webhook_id: string;
allowed_methods?: string[];
local_only?: boolean;
}
export interface ZoneTrigger extends BaseTrigger {

View File

@@ -9,6 +9,15 @@ interface CloudStatusNotLoggedIn {
http_use_ssl: boolean;
}
export interface GoogleEntityConfig {
should_expose?: boolean | null;
disable_2fa?: boolean;
}
export interface AlexaEntityConfig {
should_expose?: boolean | null;
}
export interface CertificateInformation {
common_name: string;
expire_date: string;
@@ -21,6 +30,14 @@ export interface CloudPreferences {
remote_enabled: boolean;
google_secure_devices_pin: string | undefined;
cloudhooks: { [webhookId: string]: CloudWebhook };
google_default_expose: string[] | null;
google_entity_configs: {
[entityId: string]: GoogleEntityConfig;
};
alexa_default_expose: string[] | null;
alexa_entity_configs: {
[entityId: string]: AlexaEntityConfig;
};
alexa_report_state: boolean;
google_report_state: boolean;
tts_default_voice: [string, string];
@@ -40,13 +57,6 @@ export interface CloudStatusLoggedIn {
remote_domain: string | undefined;
remote_connected: boolean;
remote_certificate: undefined | CertificateInformation;
remote_certificate_status:
| null
| "error"
| "generating"
| "loaded"
| "loading"
| "ready";
http_use_ssl: boolean;
active_subscription: boolean;
}
@@ -140,8 +150,10 @@ export const updateCloudPref = (
prefs: {
google_enabled?: CloudPreferences["google_enabled"];
alexa_enabled?: CloudPreferences["alexa_enabled"];
alexa_default_expose?: CloudPreferences["alexa_default_expose"];
alexa_report_state?: CloudPreferences["alexa_report_state"];
google_report_state?: CloudPreferences["google_report_state"];
google_default_expose?: CloudPreferences["google_default_expose"];
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
tts_default_voice?: CloudPreferences["tts_default_voice"];
}
@@ -153,14 +165,25 @@ export const updateCloudPref = (
export const updateCloudGoogleEntityConfig = (
hass: HomeAssistant,
entity_id: string,
disable_2fa: boolean
entityId: string,
values: GoogleEntityConfig
) =>
hass.callWS({
hass.callWS<GoogleEntityConfig>({
type: "cloud/google_assistant/entities/update",
entity_id,
disable_2fa,
entity_id: entityId,
...values,
});
export const cloudSyncGoogleAssistant = (hass: HomeAssistant) =>
hass.callApi("POST", "cloud/google_actions/sync");
export const updateCloudAlexaEntityConfig = (
hass: HomeAssistant,
entityId: string,
values: AlexaEntityConfig
) =>
hass.callWS<AlexaEntityConfig>({
type: "cloud/alexa/entities/update",
entity_id: entityId,
...values,
});

View File

@@ -56,11 +56,6 @@ export interface AgentInfo {
attribution?: { name: string; url: string };
}
export interface Agent {
id: string;
name: string;
}
export const processConversationInput = (
hass: HomeAssistant,
text: string,
@@ -75,20 +70,9 @@ export const processConversationInput = (
language,
});
export const listAgents = (
hass: HomeAssistant
): Promise<{ agents: Agent[]; default_agent: string | null }> =>
hass.callWS({
type: "conversation/agent/list",
});
export const getAgentInfo = (
hass: HomeAssistant,
agent_id?: string
): Promise<AgentInfo> =>
export const getAgentInfo = (hass: HomeAssistant): Promise<AgentInfo> =>
hass.callWS({
type: "conversation/agent/info",
agent_id,
});
export const prepareConversation = (

View File

@@ -2,7 +2,6 @@ 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";
@@ -115,12 +114,10 @@ export function computeCoverPositionStateDisplay(
locale: FrontendLocaleData,
position?: number
) {
const statePosition = stateActive(stateObj)
? stateObj.attributes.current_position ??
stateObj.attributes.current_tilt_position
: undefined;
const currentPosition = position ?? statePosition;
const currentPosition =
position ??
stateObj.attributes.current_position ??
stateObj.attributes.current_tilt_position;
return currentPosition && currentPosition !== 100
? `${Math.round(currentPosition)}${blankBeforePercent(locale)}%`

View File

@@ -9,7 +9,6 @@ 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";
@@ -70,7 +69,7 @@ export function fanSpeedToPercentage(
if (speedValue === -1) {
return 0;
}
return Math.floor(speedValue * step);
return Math.round(speedValue * step);
}
export function computeFanSpeedCount(stateObj: FanEntity): number {
@@ -100,12 +99,9 @@ export function computeFanSpeedStateDisplay(
locale: FrontendLocaleData,
speed?: number
) {
const percentage = stateActive(stateObj)
? stateObj.attributes.percentage
: undefined;
const currentSpeed = speed ?? percentage;
const currentSpeed = speed ?? stateObj.attributes.percentage;
return currentSpeed
? `${Math.floor(currentSpeed)}${blankBeforePercent(locale)}%`
? `${Math.round(currentSpeed)}${blankBeforePercent(locale)}%`
: "";
}

View File

@@ -9,14 +9,5 @@ export interface GoogleEntity {
export const fetchCloudGoogleEntities = (hass: HomeAssistant) =>
hass.callWS<GoogleEntity[]>({ type: "cloud/google_assistant/entities" });
export const fetchCloudGoogleEntity = (
hass: HomeAssistant,
entity_id: string
) =>
hass.callWS<GoogleEntity>({
type: "cloud/google_assistant/entities/get",
entity_id,
});
export const syncCloudGoogleEntities = (hass: HomeAssistant) =>
hass.callApi("POST", "cloud/google_actions/sync");

View File

@@ -381,23 +381,3 @@ export const fetchAddonInfo = (
? `/store/addons/${addonSlug}` // Use /store/addons when add-on is not installed
: `/addons/${addonSlug}/info` // Use /addons when add-on is installed
);
export const rebuildLocalAddon = async (
hass: HomeAssistant,
slug: string
): Promise<void> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS<void>({
type: "supervisor/api",
endpoint: `/addons/${slug}/rebuild`,
method: "post",
timeout: null,
});
}
return (
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}rebuild`
)
).data;
};

View File

@@ -28,19 +28,8 @@ export interface HassioHassOSInfo {
data_disk: string;
}
export interface Datadisk {
name: string;
vendor: string;
model: string;
serial: string;
size: number;
id: string;
dev_path: string;
}
export interface DatadiskList {
devices: string[];
disks: Datadisk[];
}
export const fetchHassioHostInfo = async (

View File

@@ -291,7 +291,7 @@ const processTimelineEntity = (
state_localize: computeStateDisplayFromEntityAttributes(
localize,
language,
entities[entityId],
entities,
entityId,
{
...(state.a || first.a),

View File

@@ -14,7 +14,6 @@ export type Selector =
| BooleanSelector
| ColorRGBSelector
| ColorTempSelector
| ConversationAgentSelector
| ConfigEntrySelector
| ConstantSelector
| DateSelector
@@ -35,12 +34,10 @@ export type Selector =
| StateSelector
| StatisticSelector
| StringSelector
| STTSelector
| TargetSelector
| TemplateSelector
| ThemeSelector
| TimeSelector
| TTSSelector
| UiActionSelector
| UiColorSelector;
@@ -88,11 +85,6 @@ export interface ColorTempSelector {
} | null;
}
export interface ConversationAgentSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
conversation_agent: {} | null;
}
export interface ConfigEntrySelector {
config_entry: {
integration?: string;
@@ -302,10 +294,6 @@ export interface StringSelector {
} | null;
}
export interface STTSelector {
stt: { language?: string } | null;
}
export interface TargetSelector {
target: {
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
@@ -327,19 +315,15 @@ export interface TimeSelector {
time: {} | null;
}
export interface TTSSelector {
tts: { language?: string } | null;
}
export interface UiActionSelector {
ui_action: {
"ui-action": {
actions?: UiAction[];
} | null;
}
export interface UiColorSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
ui_color: {} | null;
"ui-color": {} | null;
}
export const filterSelectorDevices = (
@@ -405,8 +389,8 @@ export const filterSelectorEntities = (
if (filterSupportedFeature) {
if (
!ensureArray(filterSupportedFeature).some((feature) =>
supportsFeature(entity, feature)
ensureArray(filterSupportedFeature).some(
(feature) => !supportsFeature(entity, feature)
)
) {
return false;

View File

@@ -1,5 +1,3 @@
import { HomeAssistant } from "../types";
export interface SpeechMetadata {
language: string;
format: "wav" | "ogg";
@@ -17,17 +15,3 @@ export interface SpeechMetadata {
| 48000;
channel: 1 | 2;
}
export interface STTEngine {
engine_id: string;
language_supported?: boolean;
}
export const listSTTEngines = (
hass: HomeAssistant,
language?: string
): Promise<{ providers: STTEngine[] }> =>
hass.callWS({
type: "stt/engine/list",
language,
});

View File

@@ -18,7 +18,6 @@ export interface ThreadDataSet {
network_name: string;
extended_pan_id?: string;
pan_id?: string;
channel?: number;
}
export interface ThreadRouterDiscoveryEvent {

View File

@@ -1,15 +1,5 @@
import { HomeAssistant } from "../types";
export interface TTSEngine {
engine_id: string;
language_supported?: boolean;
}
export interface TTSVoice {
voice_id: string;
name: string;
}
export const convertTextToSpeech = (
hass: HomeAssistant,
data: {
@@ -28,23 +18,3 @@ export const isTTSMediaSource = (mediaContentId: string) =>
export const getProviderFromTTSMediaSource = (mediaContentId: string) =>
mediaContentId.substring(TTS_MEDIA_SOURCE_PREFIX.length);
export const listTTSEngines = (
hass: HomeAssistant,
language?: string
): Promise<{ providers: TTSEngine[] }> =>
hass.callWS({
type: "tts/engine/list",
language,
});
export const listTTSVoices = (
hass: HomeAssistant,
engine_id: string,
language: string
): Promise<{ voices: TTSVoice[] }> =>
hass.callWS({
type: "tts/engine/voices",
engine_id,
language,
});

View File

@@ -1,45 +0,0 @@
import { HomeAssistant } from "../types";
export const voiceAssistants = {
conversation: { domain: "assist_pipeline", name: "Assist" },
"cloud.alexa": {
domain: "alexa",
name: "Amazon Alexa",
},
"cloud.google_assistant": {
domain: "google_assistant",
name: "Google Assistant",
},
} as const;
export const voiceAssistantKeys = Object.keys(voiceAssistants);
export const setExposeNewEntities = (
hass: HomeAssistant,
assistant: string,
expose_new: boolean
) =>
hass.callWS({
type: "homeassistant/expose_new_entities/set",
assistant,
expose_new,
});
export const getExposeNewEntities = (hass: HomeAssistant, assistant: string) =>
hass.callWS<{ expose_new: boolean }>({
type: "homeassistant/expose_new_entities/get",
assistant,
});
export const exposeEntities = (
hass: HomeAssistant,
assistants: string[],
entity_ids: string[],
should_expose: boolean
) =>
hass.callWS({
type: "homeassistant/expose_entity",
assistants,
entity_ids,
should_expose,
});

174
src/data/voice_assistant.ts Normal file
View File

@@ -0,0 +1,174 @@
import type { HomeAssistant } from "../types";
import type { ConversationResult } from "./conversation";
import type { ResolvedMediaSource } from "./media_source";
import type { SpeechMetadata } from "./stt";
interface PipelineEventBase {
timestamp: string;
}
interface PipelineRunStartEvent extends PipelineEventBase {
type: "run-start";
data: {
pipeline: string;
language: string;
runner_data: {
stt_binary_handler_id: number | null;
timeout: number;
};
};
}
interface PipelineRunEndEvent extends PipelineEventBase {
type: "run-end";
data: Record<string, never>;
}
interface PipelineErrorEvent extends PipelineEventBase {
type: "error";
data: {
code: string;
message: string;
};
}
interface PipelineSTTStartEvent extends PipelineEventBase {
type: "stt-start";
data: {
engine: string;
metadata: SpeechMetadata;
};
}
interface PipelineSTTEndEvent extends PipelineEventBase {
type: "stt-end";
data: {
stt_output: { text: string };
};
}
interface PipelineIntentStartEvent extends PipelineEventBase {
type: "intent-start";
data: {
engine: string;
intent_input: string;
};
}
interface PipelineIntentEndEvent extends PipelineEventBase {
type: "intent-end";
data: {
intent_output: ConversationResult;
};
}
interface PipelineTTSStartEvent extends PipelineEventBase {
type: "tts-start";
data: {
engine: string;
tts_input: string;
};
}
interface PipelineTTSEndEvent extends PipelineEventBase {
type: "tts-end";
data: {
tts_output: ResolvedMediaSource;
};
}
type PipelineRunEvent =
| PipelineRunStartEvent
| PipelineRunEndEvent
| PipelineErrorEvent
| PipelineSTTStartEvent
| PipelineSTTEndEvent
| PipelineIntentStartEvent
| PipelineIntentEndEvent
| PipelineTTSStartEvent
| PipelineTTSEndEvent;
interface PipelineRunOptions {
start_stage: "stt" | "intent" | "tts";
end_stage: "stt" | "intent" | "tts";
language?: string;
pipeline?: string;
input?: { text: string };
conversation_id?: string | null;
}
export interface PipelineRun {
init_options: PipelineRunOptions;
events: PipelineRunEvent[];
stage: "ready" | "stt" | "intent" | "tts" | "done" | "error";
run: PipelineRunStartEvent["data"];
error?: PipelineErrorEvent["data"];
stt?: PipelineSTTStartEvent["data"] & Partial<PipelineSTTEndEvent["data"]>;
intent?: PipelineIntentStartEvent["data"] &
Partial<PipelineIntentEndEvent["data"]>;
tts?: PipelineTTSStartEvent["data"] & Partial<PipelineTTSEndEvent["data"]>;
}
export const runPipelineFromText = (
hass: HomeAssistant,
callback: (event: PipelineRun) => void,
options: PipelineRunOptions
) => {
let run: PipelineRun | undefined;
const unsubProm = hass.connection.subscribeMessage<PipelineRunEvent>(
(updateEvent) => {
if (updateEvent.type === "run-start") {
run = {
init_options: options,
stage: "ready",
run: updateEvent.data,
error: undefined,
stt: undefined,
intent: undefined,
tts: undefined,
events: [updateEvent],
};
callback(run);
return;
}
if (!run) {
// eslint-disable-next-line no-console
console.warn(
"Received unexpected event before receiving session",
updateEvent
);
return;
}
if (updateEvent.type === "stt-start") {
run = { ...run, stage: "stt", stt: updateEvent.data };
} else if (updateEvent.type === "stt-end") {
run = { ...run, stt: { ...run.stt!, ...updateEvent.data } };
} else if (updateEvent.type === "intent-start") {
run = { ...run, stage: "intent", intent: updateEvent.data };
} else if (updateEvent.type === "intent-end") {
run = { ...run, intent: { ...run.intent!, ...updateEvent.data } };
} else if (updateEvent.type === "tts-start") {
run = { ...run, stage: "tts", tts: updateEvent.data };
} else if (updateEvent.type === "tts-end") {
run = { ...run, tts: { ...run.tts!, ...updateEvent.data } };
} else if (updateEvent.type === "run-end") {
run = { ...run, stage: "done" };
unsubProm.then((unsub) => unsub());
} else if (updateEvent.type === "error") {
run = { ...run, stage: "error", error: updateEvent.data };
unsubProm.then((unsub) => unsub());
} else {
run = { ...run };
}
run.events = [...run.events, updateEvent];
callback(run);
},
{
...options,
type: "voice_assistant/run",
}
);
return unsubProm;
};

View File

@@ -1,13 +1,16 @@
import "@material/mwc-button/mwc-button";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { mdiDeleteOutline, mdiPlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-alert";
import "../../components/ha-area-picker";
import "../../components/ha-dialog";
import "../../components/ha-textfield";
import type { HaTextField } from "../../components/ha-textfield";
import { haStyle, haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { AliasesDialogParams } from "./show-dialog-aliases";
import "../../components/ha-aliases-editor";
@customElement("dialog-aliases")
class DialogAliases extends LitElement {
@@ -54,11 +57,43 @@ class DialogAliases extends LitElement {
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<ha-aliases-editor
.hass=${this.hass}
.aliases=${this._aliases}
@value-changed=${this._aliasesChanged}
></ha-aliases-editor>
<div class="form">
${this._aliases.map(
(alias, index) => html`
<div class="layout horizontal center-center row">
<ha-textfield
dialogInitialFocus=${index}
.index=${index}
class="flex-auto"
.label=${this.hass!.localize(
"ui.dialogs.aliases.input_label",
{ number: index + 1 }
)}
.value=${alias}
?data-last=${index === this._aliases.length - 1}
@input=${this._editAlias}
@keydown=${this._keyDownAlias}
></ha-textfield>
<ha-icon-button
.index=${index}
slot="navigationIcon"
label=${this.hass!.localize(
"ui.dialogs.aliases.remove_alias",
{ number: index + 1 }
)}
@click=${this._removeAlias}
.path=${mdiDeleteOutline}
></ha-icon-button>
</div>
`
)}
<div class="layout horizontal center-center">
<mwc-button @click=${this._addAlias}>
${this.hass!.localize("ui.dialogs.aliases.add_alias")}
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-button>
</div>
</div>
</div>
<mwc-button
slot="secondaryAction"
@@ -78,8 +113,32 @@ class DialogAliases extends LitElement {
`;
}
private _aliasesChanged(ev: CustomEvent): void {
this._aliases = ev.detail.value;
private async _addAlias() {
this._aliases = [...this._aliases, ""];
await this.updateComplete;
const field = this.shadowRoot?.querySelector(`ha-textfield[data-last]`) as
| HaTextField
| undefined;
field?.focus();
}
private async _editAlias(ev: Event) {
const index = (ev.target as any).index;
this._aliases[index] = (ev.target as any).value;
}
private async _keyDownAlias(ev: KeyboardEvent) {
if (ev.key === "Enter") {
ev.stopPropagation();
this._addAlias();
}
}
private async _removeAlias(ev: Event) {
const index = (ev.target as any).index;
const aliases = [...this._aliases];
aliases.splice(index, 1);
this._aliases = aliases;
}
private async _updateAliases(): Promise<void> {

View File

@@ -0,0 +1,117 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-formfield";
import "../../components/ha-switch";
import { domainToName } from "../../data/integration";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { HassDialog } from "../make-dialog-manager";
import { HaDomainTogglerDialogParams } from "./show-dialog-domain-toggler";
@customElement("dialog-domain-toggler")
class DomainTogglerDialog
extends LitElement
implements HassDialog<HaDomainTogglerDialogParams>
{
public hass!: HomeAssistant;
@state() private _params?: HaDomainTogglerDialogParams;
public showDialog(params: HaDomainTogglerDialogParams): void {
this._params = params;
}
public closeDialog() {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
const domains = this._params.domains
.map((domain) => [domainToName(this.hass.localize, domain), domain])
.sort();
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
hideActions
.heading=${createCloseHeading(
this.hass,
this._params.title ||
this.hass.localize("ui.dialogs.domain_toggler.title")
)}
>
${this._params.description
? html`<div class="description">${this._params.description}</div>`
: ""}
<div class="domains">
${domains.map(
(domain) =>
html`
<ha-formfield .label=${domain[0]}>
<ha-switch
.domain=${domain[1]}
.checked=${!this._params!.exposedDomains ||
this._params!.exposedDomains.includes(domain[1])}
@change=${this._handleSwitch}
>
</ha-switch>
</ha-formfield>
<mwc-button .domain=${domain[1]} @click=${this._handleReset}>
${this.hass.localize(
"ui.dialogs.domain_toggler.reset_entities"
)}
</mwc-button>
`
)}
</div>
</ha-dialog>
`;
}
private _handleSwitch(ev) {
this._params!.toggleDomain(ev.currentTarget.domain, ev.target.checked);
ev.currentTarget.blur();
}
private _handleReset(ev) {
this._params!.resetDomain(ev.currentTarget.domain);
ev.currentTarget.blur();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
}
.description {
margin-bottom: 8px;
}
.domains {
display: grid;
grid-template-columns: auto auto;
grid-row-gap: 8px;
align-items: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-domain-toggler": DomainTogglerDialog;
}
}

View File

@@ -0,0 +1,23 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface HaDomainTogglerDialogParams {
title?: string;
description?: string;
domains: string[];
exposedDomains: string[] | null;
toggleDomain: (domain: string, turnOn: boolean) => void;
resetDomain: (domain: string) => void;
}
export const loadDomainTogglerDialog = () => import("./dialog-domain-toggler");
export const showDomainTogglerDialog = (
element: HTMLElement,
dialogParams: HaDomainTogglerDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-domain-toggler",
dialogImport: loadDomainTogglerDialog,
dialogParams,
});
};

View File

@@ -62,7 +62,7 @@ export const showDialog = async (
dialogParams: unknown,
dialogImport?: () => Promise<unknown>,
addHistory = true
): Promise<boolean> => {
) => {
if (!(dialogTag in LOADED)) {
if (!dialogImport) {
if (__DEV__) {
@@ -71,7 +71,7 @@ export const showDialog = async (
"Asked to show dialog that's not loaded and can't be imported"
);
}
return false;
return;
}
LOADED[dialogTag] = {
element: dialogImport().then(() => {
@@ -128,8 +128,6 @@ export const showDialog = async (
// so it's guaranteed to be on top of the other elements
root.appendChild(dialogElement);
dialogElement.showDialog(dialogParams);
return true;
};
export const replaceDialog = (dialogElement: HassDialog) => {

View File

@@ -40,7 +40,9 @@ export class HaMoreInfoAlarmControlPanelModes extends LitElement {
}
private _getCurrentMode(stateObj: AlarmControlPanelEntity) {
return this._modes(stateObj).find((mode) => mode === stateObj.state);
return this._modes(stateObj).find(
(mode) => ALARM_MODES[mode].state === stateObj.state
);
}
private async _setMode(mode: AlarmMode) {
@@ -84,7 +86,7 @@ export class HaMoreInfoAlarmControlPanelModes extends LitElement {
private async _valueChanged(ev: CustomEvent) {
const mode = (ev.detail as any).value as AlarmMode;
if (mode === this.stateObj!.state) return;
if (ALARM_MODES[mode].state === this.stateObj!.state) return;
const oldMode = this._getCurrentMode(this.stateObj!);
this._currentMode = mode;

View File

@@ -3,7 +3,6 @@ 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";
@@ -27,25 +26,20 @@ export class HaMoreInfoFanSpeed extends LitElement {
@property({ attribute: false }) public stateObj!: FanEntity;
@state() sliderValue?: number;
@state() speedValue?: FanSpeed;
@state() value?: number;
protected updated(changedProp: Map<string | number | symbol, unknown>): void {
if (changedProp.has("stateObj")) {
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);
this.value =
this.stateObj.attributes.percentage != null
? Math.max(Math.round(this.stateObj.attributes.percentage), 1)
: undefined;
}
}
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", {
@@ -58,8 +52,6 @@ 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,
@@ -96,11 +88,16 @@ 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=${this.speedValue}
.value=${speed}
@value-changed=${this._speedValueChanged}
.ariaLabel=${computeAttributeNameDisplay(
this.hass.localize,
@@ -122,7 +119,7 @@ export class HaMoreInfoFanSpeed extends LitElement {
vertical
min="0"
max="100"
.value=${this.sliderValue}
.value=${this.value}
.step=${this.stateObj.attributes.percentage_step ?? 1}
@value-changed=${this._valueChanged}
.ariaLabel=${computeAttributeNameDisplay(

View File

@@ -1,49 +0,0 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { ExtEntityRegistryEntry } from "../../../../data/entity_registry";
import "../../../../panels/config/voice-assistants/entity-voice-settings";
import { HomeAssistant } from "../../../../types";
@customElement("ha-more-info-view-voice-assistants")
class MoreInfoViewVoiceAssistants extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entry!: ExtEntityRegistryEntry;
@property() public params?;
protected render() {
if (!this.params) {
return nothing;
}
return html`<entity-voice-settings
.hass=${this.hass}
.entry=${this.entry}
></entity-voice-settings>`;
}
static get styles(): CSSResultGroup {
return [
css`
:host {
display: flex;
flex-direction: column;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
flex: 1;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-view-voice-assistants": MoreInfoViewVoiceAssistants;
}
}

View File

@@ -1,16 +0,0 @@
import { fireEvent } from "../../../../common/dom/fire_event";
export const loadVoiceAssistantsView = () =>
import("./ha-more-info-view-voice-assistants");
export const showVoiceAssistantsView = (
element: HTMLElement,
title: string
): void => {
fireEvent(element, "show-child-view", {
viewTag: "ha-more-info-view-voice-assistants",
viewImport: loadVoiceAssistantsView,
viewTitle: title,
viewParams: {},
});
};

View File

@@ -23,7 +23,6 @@ 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";
@@ -120,7 +119,7 @@ class MoreInfoFan extends LitElement {
const liveValue = this._liveSpeed;
const forcedState =
liveValue != null ? (liveValue ? "on" : "off") : undefined;
this._liveSpeed != null ? (this._liveSpeed ? "on" : "off") : undefined;
const stateDisplay = computeStateDisplay(
this.hass.localize,
@@ -136,7 +135,7 @@ class MoreInfoFan extends LitElement {
liveValue
);
if (positionStateDisplay && (stateActive(this.stateObj!) || liveValue)) {
if (positionStateDisplay) {
return positionStateDisplay;
}
return stateDisplay;
@@ -274,6 +273,7 @@ class MoreInfoFan extends LitElement {
supportsPresetMode && this.stateObj.attributes.preset_modes
? html`
<ha-button-menu
corner="BOTTOM_START"
@action=${this._handlePresetMode}
@closed=${stopPropagation}
fixed

View File

@@ -173,6 +173,7 @@ class MoreInfoLight extends LitElement {
${supportsEffects && this.stateObj.attributes.effect_list
? html`
<ha-button-menu
corner="BOTTOM_START"
@action=${this._handleEffectButton}
@closed=${stopPropagation}
fixed

View File

@@ -12,8 +12,6 @@ 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";
@@ -115,19 +113,11 @@ class MoreInfoVacuum extends LitElement {
</span>
<span>
<strong>
${computeAttributeValueDisplay(
this.hass.localize,
stateObj,
this.hass.locale,
this.hass.entities,
"status"
${stateObj.attributes.status ||
this.hass.localize(
`component.vacuum.entity_component._.state.${stateObj.state}`
) ||
computeStateDisplay(
this.hass.localize,
stateObj,
this.hass.locale,
this.hass.entities
)}
stateObj.state}
</strong>
</span>
</div>

View File

@@ -181,10 +181,10 @@ export class MoreInfoDialog extends LitElement {
this.setView("settings");
}
private _showChildView(ev: CustomEvent): void {
private async _showChildView(ev: CustomEvent): Promise<void> {
const view = ev.detail as ChildView;
if (view.viewImport) {
view.viewImport();
await view.viewImport();
}
this._childView = view;
}
@@ -369,14 +369,12 @@ export class MoreInfoDialog extends LitElement {
tabindex="-1"
dialogInitialFocus
@show-child-view=${this._showChildView}
@entity-entry-updated=${this._entryUpdated}
>
${this._childView
? html`
<div class="child-view">
${dynamicElement(this._childView.viewTag, {
hass: this.hass,
entry: this._entry,
params: this._childView.viewParams,
})}
</div>
@@ -403,6 +401,7 @@ export class MoreInfoDialog extends LitElement {
.hass=${this.hass}
.entityId=${this._entityId}
.entry=${this._entry}
@entity-entry-updated=${this._entryUpdated}
></ha-more-info-settings>
`
: this._currView === "related"
@@ -452,7 +451,6 @@ export class MoreInfoDialog extends LitElement {
--dialog-surface-position: static;
--dialog-content-position: static;
--dialog-content-padding: 0;
--chart-base-position: static;
}
ha-header-bar {

View File

@@ -27,7 +27,7 @@ declare global {
}
}
const statTypes: StatisticsTypes = ["min", "mean", "max"];
const statTypes: StatisticsTypes = ["state", "min", "mean", "max"];
@customElement("ha-more-info-history")
export class MoreInfoHistory extends LitElement {

View File

@@ -6,6 +6,7 @@ import "@webcomponents/scoped-custom-element-registry/scoped-custom-element-regi
import "../layouts/home-assistant";
import "../resources/ha-style";
import "../resources/roboto";
import "../util/legacy-support";
setPassiveTouchGestures(true);
setCancelSyntheticClickEvents(false);

View File

@@ -6,7 +6,6 @@ This is the entry point for providing external app stuff from app entrypoint.
*/
import { fireEvent } from "../common/dom/fire_event";
import { mainWindow } from "../common/dom/get_main_window";
import { HomeAssistantMain } from "../layouts/home-assistant-main";
import type { EMIncomingMessageCommands } from "./external_messaging";
@@ -46,15 +45,6 @@ const handleExternalMessage = (
result: null,
});
} else if (msg.command === "sidebar/toggle") {
if (mainWindow.history.state?.open) {
bus.fireMessage({
id: msg.id,
type: "result",
success: false,
error: { code: "not_allowed", message: "dialog open" },
});
return true;
}
fireEvent(hassMainEl, "hass-toggle-menu");
bus.fireMessage({
id: msg.id,
@@ -63,16 +53,10 @@ const handleExternalMessage = (
result: null,
});
} else if (msg.command === "sidebar/show") {
if (mainWindow.history.state?.open) {
bus.fireMessage({
id: msg.id,
type: "result",
success: false,
error: { code: "not_allowed", message: "dialog open" },
});
return true;
}
fireEvent(hassMainEl, "hass-toggle-menu", { open: true });
fireEvent(hassMainEl, "hass-toggle-menu", {
open: true,
screenPercentage: msg.data?.screenPercentage,
});
bus.fireMessage({
id: msg.id,
type: "result",

View File

@@ -131,6 +131,7 @@ interface EMIncomingMessageShowSidebar {
id: number;
type: "command";
command: "sidebar/show";
data?: { screenPercentage: number };
}
export type EMIncomingMessageCommands =

View File

@@ -1,9 +1,12 @@
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { property, state } from "lit/decorators";
class HaInitPage extends LitElement {
@property({ type: Boolean }) public error = false;
@state() private _showProgressIndicator = false;
@state() private _retryInSeconds = 60;
private _showProgressIndicatorTimeout?: NodeJS.Timeout;
@@ -33,7 +36,9 @@ class HaInitPage extends LitElement {
`
: html`
<div id="progress-indicator-wrapper">
<ha-circular-progress active></ha-circular-progress>
${this._showProgressIndicator
? html`<ha-circular-progress active></ha-circular-progress>`
: ""}
</div>
<div id="loading-text">Loading data</div>
`;
@@ -49,15 +54,10 @@ class HaInitPage extends LitElement {
}
}
protected willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("error") && this.error) {
import("@material/mwc-button");
}
}
protected firstUpdated() {
this._showProgressIndicatorTimeout = setTimeout(() => {
import("../components/ha-circular-progress");
this._showProgressIndicatorTimeout = setTimeout(async () => {
await import("../components/ha-circular-progress");
this._showProgressIndicator = true;
}, 5000);
this._retryInterval = setInterval(() => {

View File

@@ -3,6 +3,8 @@ import { property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { navigate } from "../common/navigate";
import { Route } from "../types";
import "./hass-error-screen";
import "./hass-loading-screen";
const extractPage = (path: string, defaultPage: string) => {
if (path === "") {
@@ -254,12 +256,10 @@ export class HassRouterPage extends ReactiveElement {
}
protected createLoadingScreen() {
import("./hass-loading-screen");
return document.createElement("hass-loading-screen");
}
protected createErrorScreen(error: string) {
import("./hass-error-screen");
const errorEl = document.createElement("hass-error-screen");
errorEl.error = error;
return errorEl;

View File

@@ -99,8 +99,6 @@ class HassSubpage extends LitElement {
display: block;
height: 100%;
background-color: var(--primary-background-color);
overflow: hidden;
position: relative;
}
:host([narrow]) {
@@ -154,7 +152,7 @@ class HassSubpage extends LitElement {
}
#fab {
position: absolute;
position: fixed;
right: calc(16px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;

View File

@@ -11,7 +11,6 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent, HASSDomEvent } from "../common/dom/fire_event";
import { listenMediaQuery } from "../common/dom/media_query";
import { toggleAttribute } from "../common/dom/toggle_attribute";
import { computeRTLDirection } from "../common/util/compute_rtl";
import "../components/ha-drawer";
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
import type { HomeAssistant, Route } from "../types";
@@ -20,7 +19,9 @@ import "./partial-panel-resolver";
declare global {
// for fire event
interface HASSDomEvents {
"hass-toggle-menu": undefined | { open?: boolean };
"hass-toggle-menu":
| undefined
| { open?: boolean; screenPercentage?: number };
"hass-edit-sidebar": EditSideBarEvent;
"hass-show-notifications": undefined;
}
@@ -62,7 +63,6 @@ export class HomeAssistantMain extends LitElement {
<ha-drawer
.type=${sidebarNarrow ? "modal" : ""}
.open=${sidebarNarrow ? this._drawerOpen : undefined}
.direction=${computeRTLDirection(this.hass)}
@MDCDrawer:closed=${this._drawerClosed}
>
<ha-sidebar
@@ -122,6 +122,10 @@ export class HomeAssistantMain extends LitElement {
}
if (this._sidebarNarrow) {
this._drawerOpen = ev.detail?.open ?? !this._drawerOpen;
const offset = ev.detail?.screenPercentage
? -256 + screen.width * (ev.detail.screenPercentage / 100)
: 0;
this.style.setProperty("--drawer-modal-left-offset", `${offset}px`);
} else {
fireEvent(this, "hass-dock-sidebar", {
dock: ev.detail?.open

View File

@@ -65,7 +65,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
`;
}
update(changedProps: PropertyValues<this>) {
update(changedProps) {
if (this.hass?.states && this.hass.config && this.hass.services) {
this.render = this.renderHass;
this.update = super.update;
@@ -74,7 +74,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
super.update(changedProps);
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._initializeHass();
setTimeout(() => registerServiceWorker(this), 1000);

View File

@@ -23,11 +23,11 @@ class OnboardingAnalytics extends LitElement {
protected render(): TemplateResult {
return html`
<p>${this.localize("ui.panel.page-onboarding.analytics.intro")}</p>
<p>${this.hass.localize("ui.panel.page-onboarding.analytics.intro")}</p>
<ha-analytics
translation_key_panel="page-onboarding"
@analytics-preferences-changed=${this._preferencesChanged}
.localize=${this.localize}
.hass=${this.hass}
.analytics=${this._analyticsDetails}
>
</ha-analytics>
@@ -41,7 +41,7 @@ class OnboardingAnalytics extends LitElement {
target="_blank"
rel="noreferrer"
>
${this.localize("ui.panel.page-onboarding.analytics.learn_more")}
${this.hass.localize("ui.panel.page-onboarding.analytics.learn_more")}
</a>
</div>
`;

View File

@@ -1,21 +1,20 @@
import "@material/mwc-button/mwc-button";
import { mdiCheck, mdiDotsHorizontal } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
TemplateResult,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
import { LocalizeFunc } from "../common/translations/localize";
import { ConfigEntry, subscribeConfigEntries } from "../data/config_entries";
import { ConfigEntry, getConfigEntries } from "../data/config_entries";
import {
getConfigFlowInProgressCollection,
localizeConfigFlowTitle,
@@ -28,8 +27,6 @@ import {
loadConfigFlowDialog,
showConfigFlowDialog,
} from "../dialogs/config-flow/show-dialog-config-flow";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { showAddIntegrationDialog } from "../panels/config/integrations/show-add-integration-dialog";
import { HomeAssistant } from "../types";
import "./action-badge";
import "./integration-badge";
@@ -43,7 +40,7 @@ const HIDDEN_DOMAINS = new Set([
]);
@customElement("onboarding-integrations")
class OnboardingIntegrations extends SubscribeMixin(LitElement) {
class OnboardingIntegrations extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public onboardingLocalize!: LocalizeFunc;
@@ -52,56 +49,30 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
@state() private _discovered?: DataEntryFlowProgress[];
public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> {
return [
subscribeConfigFlowInProgress(this.hass, (flows) => {
this._discovered = flows;
const integrations: Set<string> = new Set();
for (const flow of flows) {
// To render title placeholders
if (flow.context.title_placeholders) {
integrations.add(flow.handler);
}
private _unsubEvents?: () => void;
public connectedCallback() {
super.connectedCallback();
this.hass.loadBackendTranslation("title", undefined, true);
this._unsubEvents = subscribeConfigFlowInProgress(this.hass, (flows) => {
this._discovered = flows;
const integrations: Set<string> = new Set();
for (const flow of flows) {
// To render title placeholders
if (flow.context.title_placeholders) {
integrations.add(flow.handler);
}
this.hass.loadBackendTranslation("config", Array.from(integrations));
}),
subscribeConfigEntries(
this.hass,
(messages) => {
let fullUpdate = false;
const newEntries: ConfigEntry[] = [];
messages.forEach((message) => {
if (message.type === null || message.type === "added") {
if (HIDDEN_DOMAINS.has(message.entry.domain)) {
return;
}
newEntries.push(message.entry);
if (message.type === null) {
fullUpdate = true;
}
} else if (message.type === "removed") {
this._entries = this._entries!.filter(
(entry) => entry.entry_id !== message.entry.entry_id
);
} else if (message.type === "updated") {
if (HIDDEN_DOMAINS.has(message.entry.domain)) {
return;
}
const newEntry = message.entry;
this._entries = this._entries!.map((entry) =>
entry.entry_id === newEntry.entry_id ? newEntry : entry
);
}
});
if (!newEntries.length && !fullUpdate) {
return;
}
const existingEntries = fullUpdate ? [] : this._entries;
this._entries = [...existingEntries!, ...newEntries];
},
{ type: ["device", "hub", "service"] }
),
];
}
this.hass.loadBackendTranslation("config", Array.from(integrations));
});
}
public disconnectedCallback() {
super.disconnectedCallback();
if (this._unsubEvents) {
this._unsubEvents();
this._unsubEvents = undefined;
}
}
protected render() {
@@ -178,19 +149,25 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.hass.loadBackendTranslation("title", undefined, true);
this._scanUSBDevices();
loadConfigFlowDialog();
this._loadConfigEntries();
}
private _createFlow() {
showAddIntegrationDialog(this);
showConfigFlowDialog(this, {
dialogClosedCallback: () => {
this._loadConfigEntries();
getConfigFlowInProgressCollection(this.hass!.connection).refresh();
},
});
}
private _continueFlow(ev) {
showConfigFlowDialog(this, {
continueFlowId: ev.currentTarget.flowId,
dialogClosedCallback: () => {
this._loadConfigEntries();
getConfigFlowInProgressCollection(this.hass!.connection).refresh();
},
});
@@ -203,6 +180,18 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
await scanUSBDevices(this.hass);
}
private async _loadConfigEntries() {
const entries = await getConfigEntries(this.hass!, {
type: ["device", "hub", "service"],
});
// We filter out the config entries that are automatically created during onboarding.
// It is one that we create automatically and it will confuse the user
// if it starts showing up during onboarding.
this._entries = entries.filter(
(entry) => !HIDDEN_DOMAINS.has(entry.domain)
);
}
private async _finish() {
fireEvent(this, "onboarding-step", {
type: "integration",

View File

@@ -182,6 +182,8 @@ export default class HaAutomationActionRow extends LitElement {
: html`
<ha-button-menu
slot="icons"
fixed
corner="BOTTOM_START"
@action=${this._handleAction}
@click=${preventDefault}
>

View File

@@ -128,7 +128,11 @@ export default class HaAutomationAction extends LitElement {
`
)}
</div>
<ha-button-menu @action=${this._addAction} .disabled=${this.disabled}>
<ha-button-menu
fixed
@action=${this._addAction}
.disabled=${this.disabled}
>
<ha-button
slot="trigger"
outlined

View File

@@ -116,6 +116,8 @@ export default class HaAutomationConditionRow extends LitElement {
: html`
<ha-button-menu
slot="icons"
fixed
corner="BOTTOM_START"
@action=${this._handleAction}
@click=${preventDefault}
>

View File

@@ -180,7 +180,11 @@ export default class HaAutomationCondition extends LitElement {
`
)}
</div>
<ha-button-menu @action=${this._addCondition} .disabled=${this.disabled}>
<ha-button-menu
fixed
@action=${this._addCondition}
.disabled=${this.disabled}
>
<ha-button
slot="trigger"
outlined

View File

@@ -141,7 +141,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
</mwc-button>
`
: ""}
<ha-button-menu slot="toolbar-icon">
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}

View File

@@ -287,6 +287,7 @@ 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}

Some files were not shown because too many files have changed in this diff Show More