Compare commits

..

44 Commits

Author SHA1 Message Date
Zack
8899bba15f Justify Around Button BAse 2022-02-16 16:14:05 -06:00
Zack
ec31ea8d9c Justify Around Button Base 2022-02-16 16:13:45 -06:00
Bram Kragten
9dec0f8ccd Fix mode selection in automation editor (#11707) 2022-02-16 21:47:49 +01:00
Bram Kragten
89f4fe9d20 Convert scene action to service call (#11705)
* Convert scene action to service call

* fix describeAction

* rename to metadata

* Update script.ts
2022-02-16 20:47:21 +01:00
Josh McCarty
f43655eea5 Fixes remote icon state color (#11698) 2022-02-16 16:54:12 +01:00
Philip Allgaier
6563984fdd Convert triple dots to single char in translations (#11697) 2022-02-16 16:20:19 +01:00
Zack Barett
16d8eb0be3 Clean up some imports (#11696) 2022-02-15 12:53:20 -08:00
Paulus Schoutsen
965fc9bc4e Fix import 2022-02-15 11:16:51 -08:00
Bram Kragten
56cb958a47 Migrate all lovelace elements to mwc (#11695)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-02-15 11:09:34 -08:00
Steve Repsher
f5feb1d8aa Set initial focus for lovelace dialogs (#11667)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-15 16:42:46 +00:00
Matthias de Baat
e95065ed08 Updated text part 2 (#11686)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-02-15 09:57:26 -06:00
Bram Kragten
68a411838d This adds back mobile click accessibility (#11693) 2022-02-15 15:24:54 +00:00
Bram Kragten
ba63ab8b7a Latest paper-dropdown -> mwc-select conversion (#11692) 2022-02-15 09:11:43 -06:00
Erik Montnemery
26d4599ef4 Display transmitted messages in MQTT debug info dialog (#11531) 2022-02-15 11:18:05 +00:00
Brandon Rothweiler
d049990f04 Improve stripPrefixFromEntityName to handle colon and space separator (#11691) 2022-02-15 09:03:58 +00:00
Paulus Schoutsen
9c8d683a19 Group helpers not in an area in a single card (#11690) 2022-02-14 23:13:35 -08:00
Paulus Schoutsen
901677bbdf Bumped version to 20220214.0 2022-02-14 15:33:08 -08:00
Paulus Schoutsen
8bb2374b1b Allow uploading multiple files (#11687) 2022-02-14 17:25:23 -06:00
Paulus Schoutsen
520896a3c2 Try to keep the browsing stack when changing players in media panel (#11681) 2022-02-14 15:21:17 -08:00
Bram Kragten
92db272759 Dont exclude domain for area and device (#11689) 2022-02-14 16:56:50 -06:00
Bram Kragten
fc654d86c6 hassio fixes (#11688) 2022-02-14 22:33:12 +01:00
Bram Kragten
523afe2f6f Another round of paper-dropdown -> mwc-select conversion (#11674)
* Another round of paper-dropdown -> mwc-select conversion

* ha-pick-language-row -> Lit

* Update hui-view-editor.ts

* Cleanup imports

* hassio

* Add explicit imports
2022-02-14 20:08:18 +01:00
Zack Barett
460b9003fc Script Editor to Ha Form (#11601)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-14 11:27:29 -06:00
kpine
2ac0ad1d98 Omit Device info and actions for connected controller nodes (#11673) 2022-02-14 17:06:03 +01:00
Paulus Schoutsen
a321432175 Add TTS to media browser (#11679) 2022-02-14 07:50:44 -08:00
Zack Barett
63c9b3f830 Don't show toggle always on more info (#11640) 2022-02-14 16:21:46 +01:00
Paulus Schoutsen
806b1296b0 Limit types of media that can be uploaded to local media (#11683) 2022-02-14 15:33:21 +01:00
Steve Repsher
7f90ffa82f Set initial focus for some more dialogs (#11676) 2022-02-13 22:02:48 +01:00
Paulus Schoutsen
db33c38e21 Revert compute state display show empty string as unknown (#11677) 2022-02-13 20:26:12 +01:00
Allen Porter
a8c1fdd21e Improve robustness of hls media player (#11672) 2022-02-12 20:21:26 -08:00
lintaba
d86a18b80b hotfix history view on missing state (#11663)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-02-12 14:00:50 -08:00
Michael
bef6591548 Add WORKSPACE_DIRECTORY environment variable to devcontainer and script.core (#11477)
Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
2022-02-12 07:30:19 +01:00
Bram Kragten
e1c07f109c Filter fixes (#11664) 2022-02-11 23:24:29 +01:00
Zack Barett
fb66d224ae Numerical State to HA-Form (#11646)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-11 22:39:33 +01:00
Paulus Schoutsen
ee1fd3e865 Allow adding Zigbee/Zwave device (#11650) 2022-02-11 19:49:16 +01:00
Bram Kragten
a9bfea233c Improve search and filters on mobile + fix close button in search field (#11662)
Co-authored-by: Zack <zackbarett@hey.com>
2022-02-11 18:34:50 +00:00
Shay Levy
35cc291118 Add support for media player assumed state (#11642) 2022-02-11 08:42:22 -08:00
Zack Barett
db7cac5782 Fix Lovelace Empty Menu when not advanced or admin (#11660) 2022-02-11 10:31:45 -06:00
Zack Barett
099fa706a0 Make HA Form set required to false for selectors (#11649) 2022-02-11 14:05:07 +01:00
Zack Barett
ed84ce9692 HA Trigger to HA Form (#11645)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-10 14:12:12 -08:00
Zack Barett
9912d427f2 Geo Location Trigger to HA - Form (#11644)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-10 14:12:01 -08:00
Zack Barett
76f574f875 Convert Sun to Ha Form (#11647)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-10 14:11:57 -08:00
Zack Barett
ac90bb7088 MQTT Trigger to Ha-Form (#11643)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-10 14:11:45 -08:00
Zack Barett
ce9f83e9a2 Time Pattern to HA Form (#11648) 2022-02-10 14:11:29 -08:00
156 changed files with 3087 additions and 2266 deletions

View File

@@ -16,6 +16,9 @@
"runem.lit-plugin",
"ms-python.vscode-pylance"
],
"containerEnv": {
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
},
"settings": {
"terminal.integrated.shell.linux": "/bin/bash",
"files.eol": "\n",

View File

@@ -33,6 +33,10 @@ module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
require.resolve(
path.resolve(paths.polymer_dir, "src/components/ha-icon.ts")
),
isHassioBuild &&
require.resolve(
path.resolve(paths.polymer_dir, "src/components/ha-icon-picker.ts")
),
].filter(Boolean);
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({

View File

@@ -2,8 +2,3 @@ import "../../src/resources/ha-style";
import "../../src/resources/roboto";
import "../../src/resources/safari-14-attachshadow-patch";
import "./ha-demo";
/* polyfill for paper-dropdown */
setTimeout(() => {
import("web-animations-js/web-animations-next-lite.min");
}, 1000);

View File

@@ -61,6 +61,12 @@ const SCHEMAS: {
select: { options: ["Everyone Home", "Some Home", "All gone"] },
},
},
{
name: "icon",
selector: {
icon: {},
},
},
],
},
{

View File

@@ -72,6 +72,7 @@ const SCHEMAS: {
name: "Select",
selector: { select: { options: ["Option 1", "Option 2"] } },
},
icon: { name: "Icon", selector: { icon: {} } },
},
},
];

View File

@@ -221,13 +221,14 @@ class HassioAddonStore extends LitElement {
margin-top: 24px;
}
.search {
padding: 0 16px;
background: var(--sidebar-background-color);
border-bottom: 1px solid var(--divider-color);
position: sticky;
top: 0;
z-index: 2;
}
.search search-input {
position: relative;
top: 2px;
search-input {
display: block;
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
}
.advanced {
padding: 12px;

View File

@@ -1,7 +1,6 @@
import "@material/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@material/mwc-select";
import "@material/mwc-list/mwc-list-item";
import {
css,
CSSResultGroup,
@@ -11,7 +10,7 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "web-animations-js/web-animations-next-lite.min";
import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card";
@@ -57,49 +56,44 @@ class HassioAddonAudio extends LitElement {
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<paper-dropdown-menu
${this._inputDevices &&
html`<mwc-select
.label=${this.supervisor.localize(
"addon.configuration.audio.input"
)}
@iron-select=${this._setInputDevice}
@selected=${this._setInputDevice}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._selectedInput!}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="device"
.selected=${this._selectedInput!}
>
${this._inputDevices &&
this._inputDevices.map(
(item) => html`
<paper-item device=${item.device || ""}>
${item.name}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
<paper-dropdown-menu
${this._inputDevices.map(
(item) => html`
<mwc-list-item .value=${item.device || ""}>
${item.name}
</mwc-list-item>
`
)}
</mwc-select>`}
${this._outputDevices &&
html`<mwc-select
.label=${this.supervisor.localize(
"addon.configuration.audio.output"
)}
@iron-select=${this._setOutputDevice}
@selected=${this._setOutputDevice}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._selectedOutput!}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="device"
.selected=${this._selectedOutput!}
>
${this._outputDevices &&
this._outputDevices.map(
(item) => html`
<paper-item device=${item.device || ""}
>${item.name}</paper-item
>
`
)}
</paper-listbox>
</paper-dropdown-menu>
${this._outputDevices.map(
(item) => html`
<mwc-list-item .value=${item.device || ""}
>${item.name}</mwc-list-item
>
`
)}
</mwc-select>`}
</div>
<div class="card-actions">
<ha-progress-button @click=${this._saveSettings}>
@@ -116,8 +110,7 @@ class HassioAddonAudio extends LitElement {
hassioStyle,
css`
:host,
ha-card,
paper-dropdown-menu {
ha-card {
display: block;
}
paper-item {
@@ -126,24 +119,30 @@ class HassioAddonAudio extends LitElement {
.card-actions {
text-align: right;
}
mwc-select {
width: 100%;
}
mwc-select:last-child {
margin-top: 8px;
}
`,
];
}
protected update(changedProperties: PropertyValues): void {
super.update(changedProperties);
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("addon")) {
this._addonChanged();
}
}
private _setInputDevice(ev): void {
const device = ev.detail.item.getAttribute("device");
const device = ev.target.value;
this._selectedInput = device;
}
private _setOutputDevice(ev): void {
const device = ev.detail.item.getAttribute("device");
const device = ev.target.value;
this._selectedOutput = device;
}

View File

@@ -148,7 +148,6 @@ export class HassioUpdate extends LitElement {
}
ha-settings-row {
padding: 0;
--paper-item-body-two-line-min-height: 32px;
}
`,
];

View File

@@ -1,6 +1,5 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -90,18 +89,19 @@ class HassioDatadiskDialog extends LitElement {
)}
<br /><br />
<paper-dropdown-menu
<mwc-select
.label=${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.select_device"
)}
@value-changed=${this._select_device}
@selected=${this._select_device}
>
<paper-listbox slot="dropdown-content">
${this.devices.map(
(device) => html`<paper-item>${device}</paper-item>`
)}
</paper-listbox>
</paper-dropdown-menu>
${this.devices.map(
(device) =>
html`<mwc-list-item .value=${device}
>${device}</mwc-list-item
>`
)}
</mwc-select>
`
: this.devices === undefined
? this.dialogParams.supervisor.localize(
@@ -130,8 +130,8 @@ class HassioDatadiskDialog extends LitElement {
`;
}
private _select_device(event) {
this.selectedDevice = event.detail.value;
private _select_device(ev) {
this.selectedDevice = ev.target.value;
}
private async _moveDatadisk() {
@@ -156,7 +156,7 @@ class HassioDatadiskDialog extends LitElement {
haStyle,
haStyleDialog,
css`
paper-dropdown-menu {
mwc-select {
width: 100%;
}
ha-circular-progress {

View File

@@ -178,7 +178,7 @@ class HassioHardwareDialog extends LitElement {
padding: 0.2em 0.4em;
}
search-input {
margin: 0 16px;
margin: 8px 16px 0;
display: block;
}
.device-property {

View File

@@ -1,7 +1,4 @@
import "@material/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../src/components/buttons/ha-progress-button";
@@ -73,24 +70,19 @@ class HassioSupervisorLog extends LitElement {
: ""}
${this.hass.userData?.showAdvanced
? html`
<paper-dropdown-menu
<mwc-select
.label=${this.supervisor.localize("system.log.log_provider")}
@iron-select=${this._setLogProvider}
@selected=${this._setLogProvider}
.value=${this._selectedLogProvider}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="provider"
.selected=${this._selectedLogProvider}
>
${logProviders.map(
(provider) => html`
<paper-item provider=${provider.key}>
${provider.name}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
${logProviders.map(
(provider) => html`
<mwc-list-item .value=${provider.key}>
${provider.name}
</mwc-list-item>
`
)}
</mwc-select>
`
: ""}
@@ -110,7 +102,7 @@ class HassioSupervisorLog extends LitElement {
}
private async _setLogProvider(ev): Promise<void> {
const provider = ev.detail.item.getAttribute("provider");
const provider = ev.target.value;
this._selectedLogProvider = provider;
this._loadData();
}
@@ -153,9 +145,9 @@ class HassioSupervisorLog extends LitElement {
pre {
white-space: pre-wrap;
}
paper-dropdown-menu {
padding: 0 2%;
width: 96%;
mwc-select {
width: 100%;
margin-bottom: 4px;
}
`,
];

View File

@@ -10,7 +10,6 @@ import {
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/common/search/search-input";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-button-menu";

View File

@@ -4,6 +4,8 @@
# Stop on errors
set -e
WD="${WORKSPACE_DIRECTORY:=/workspaces/frontend}"
if [ -z "${DEVCONTAINER}" ]; then
echo "This task should only run inside a devcontainer, for local install HA Core in a venv."
exit 1
@@ -16,9 +18,9 @@ if [ -z $(which hass) ]; then
git+git://github.com/home-assistant/home-assistant.git@dev
fi
if [ ! -d "/workspaces/frontend/config" ]; then
if [ ! -d "${WD}/config" ]; then
echo "Creating default configuration."
mkdir -p "/workspaces/frontend/config";
mkdir -p "${WD}/config";
hass --script ensure_config -c config
echo "demo:
@@ -26,24 +28,24 @@ logger:
default: info
logs:
homeassistant.components.frontend: debug
" >> /workspaces/frontend/config/configuration.yaml
" >> "${WD}/config/configuration.yaml"
if [ ! -z "${HASSIO}" ]; then
echo "
# frontend:
# development_repo: /workspaces/frontend
# development_repo: ${WD}
hassio:
development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml
development_repo: ${WD}" >> "${WD}/config/configuration.yaml"
else
echo "
frontend:
development_repo: /workspaces/frontend
development_repo: ${WD}
# hassio:
# development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml
# development_repo: ${WD}" >> "${WD}/config/configuration.yaml"
fi
fi
hass -c /workspaces/frontend/config
hass -c "${WD}/config"

View File

@@ -1,6 +1,6 @@
[metadata]
name = home-assistant-frontend
version = 20220203.0
version = 20220214.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0

View File

@@ -1,4 +1,4 @@
import { HomeAssistant } from "../../types";
import type { HomeAssistant } from "../../types";
export const canToggleDomain = (hass: HomeAssistant, domain: string) => {
const services = hass.services[domain];

View File

@@ -1,14 +1,30 @@
import { HassEntity } from "home-assistant-js-websocket";
import { HomeAssistant } from "../../types";
import type { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../../types";
import { canToggleDomain } from "./can_toggle_domain";
import { computeStateDomain } from "./compute_state_domain";
import { supportsFeature } from "./supports-feature";
export const canToggleState = (hass: HomeAssistant, stateObj: HassEntity) => {
const domain = computeStateDomain(stateObj);
if (domain === "group") {
return stateObj.state === "on" || stateObj.state === "off";
if (
stateObj.attributes?.entity_id?.some((entity) => {
const entityStateObj = hass.states[entity];
if (!entityStateObj) {
return false;
}
const entityDomain = computeStateDomain(entityStateObj);
return canToggleDomain(hass, entityDomain);
})
) {
return stateObj.state === "on" || stateObj.state === "off";
}
return false;
}
if (domain === "climate") {
return supportsFeature(stateObj, 4096);
}

View File

@@ -123,7 +123,11 @@ export const computeStateDisplay = (
domain === "scene" ||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp")
) {
return formatDateTime(new Date(compareState), locale);
try {
return formatDateTime(new Date(compareState), locale);
} catch (_err) {
return compareState;
}
}
return (

View File

@@ -1,24 +1,32 @@
const SUFFIXES = [" ", ": "];
/**
* Strips a device name from an entity name.
* @param entityName the entity name
* @param lowerCasedPrefixWithSpaceSuffix the prefix to strip, lower cased with a space suffix
* @param lowerCasedPrefix the prefix to strip, lower cased
* @returns
*/
export const stripPrefixFromEntityName = (
entityName: string,
lowerCasedPrefixWithSpaceSuffix: string
lowerCasedPrefix: string
) => {
if (!entityName.toLowerCase().startsWith(lowerCasedPrefixWithSpaceSuffix)) {
return undefined;
const lowerCasedEntityName = entityName.toLowerCase();
for (const suffix of SUFFIXES) {
const lowerCasedPrefixWithSuffix = `${lowerCasedPrefix}${suffix}`;
if (lowerCasedEntityName.startsWith(lowerCasedPrefixWithSuffix)) {
const newName = entityName.substring(lowerCasedPrefixWithSuffix.length);
// If first word already has an upper case letter (e.g. from brand name)
// leave as-is, otherwise capitalize the first word.
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
? newName
: newName[0].toUpperCase() + newName.slice(1);
}
}
const newName = entityName.substring(lowerCasedPrefixWithSpaceSuffix.length);
// If first word already has an upper case letter (e.g. from brand name)
// leave as-is, otherwise capitalize the first word.
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
? newName
: newName[0].toUpperCase() + newName.slice(1);
return undefined;
};
const hasUpperCase = (str: string): boolean => str.toLowerCase() !== str;

View File

@@ -14,6 +14,9 @@ class SearchInput extends LitElement {
@property() public filter?: string;
@property({ type: Boolean })
public suffix = false;
@property({ type: Boolean })
public autofocus = false;
@@ -33,7 +36,7 @@ class SearchInput extends LitElement {
.label=${this.label || "Search"}
.value=${this.filter || ""}
.icon=${true}
.iconTrailing=${this.filter}
.iconTrailing=${this.filter || this.suffix}
@input=${this._filterInputChanged}
>
<slot name="prefix" slot="leadingIcon">
@@ -43,16 +46,18 @@ class SearchInput extends LitElement {
.path=${mdiMagnify}
></ha-svg-icon>
</slot>
${this.filter &&
html`
<ha-icon-button
slot="trailingIcon"
@click=${this._clearSearch}
.label=${this.hass.localize("ui.common.clear")}
.path=${mdiClose}
class="clear-button"
></ha-icon-button>
`}
<div class="trailing" slot="trailingIcon">
${this.filter &&
html`
<ha-icon-button
@click=${this._clearSearch}
.label=${this.hass.localize("ui.common.clear")}
.path=${mdiClose}
class="clear-button"
></ha-icon-button>
`}
<slot name="suffix"></slot>
</div>
</ha-textfield>
`;
}
@@ -81,15 +86,16 @@ class SearchInput extends LitElement {
ha-svg-icon {
outline: none;
}
ha-icon-button {
--mdc-icon-button-size: 24px;
}
.clear-button {
--mdc-icon-size: 20px;
}
ha-textfield {
display: inherit;
}
.trailing {
display: flex;
align-items: center;
}
`;
}
}

View File

@@ -15,6 +15,7 @@ export const iconColorCSS = css`
ha-state-icon[data-domain="media_player"][data-state="on"],
ha-state-icon[data-domain="media_player"][data-state="paused"],
ha-state-icon[data-domain="media_player"][data-state="playing"],
ha-state-icon[data-domain="remote"][data-state="on"],
ha-state-icon[data-domain="script"][data-state="on"],
ha-state-icon[data-domain="sun"][data-state="above_horizon"],
ha-state-icon[data-domain="switch"][data-state="on"],

View File

@@ -1,6 +1,3 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";

View File

@@ -4,6 +4,7 @@ import { mdiFilterVariant } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, 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 { computeDeviceName } from "../data/device_registry";
import { findRelated, RelatedResult } from "../data/search";
@@ -65,6 +66,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
.fullwidth=${this.narrow}
.corner=${this.corner}
@closed=${this._onClosed}
@input=${stopPropagation}
>
<ha-area-picker
.label=${this.hass.localize(
@@ -74,6 +76,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
.value=${this.value?.area}
no-add
@value-changed=${this._areaPicked}
@click=${this._preventDefault}
></ha-area-picker>
<ha-device-picker
.label=${this.hass.localize(
@@ -82,6 +85,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
.hass=${this.hass}
.value=${this.value?.device}
@value-changed=${this._devicePicked}
@click=${this._preventDefault}
></ha-device-picker>
<ha-entity-picker
.label=${this.hass.localize(
@@ -91,6 +95,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
.value=${this.value?.entity}
.excludeDomains=${this.excludeDomains}
@value-changed=${this._entityPicked}
@click=${this._preventDefault}
></ha-entity-picker>
</mwc-menu-surface>
`;
@@ -103,11 +108,17 @@ export class HaRelatedFilterButtonMenu extends LitElement {
this._open = true;
}
private _onClosed(): void {
private _onClosed(ev): void {
ev.stopPropagation();
this._open = false;
}
private _preventDefault(ev) {
ev.preventDefault();
}
private async _entityPicked(ev: CustomEvent) {
ev.stopPropagation();
const entityId = ev.detail.value;
if (!entityId) {
fireEvent(this, "related-changed", { value: undefined });
@@ -127,6 +138,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
}
private async _devicePicked(ev: CustomEvent) {
ev.stopPropagation();
const deviceId = ev.detail.value;
if (!deviceId) {
fireEvent(this, "related-changed", { value: undefined });
@@ -150,6 +162,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
}
private async _areaPicked(ev: CustomEvent) {
ev.stopPropagation();
const areaId = ev.detail.value;
if (!areaId) {
fireEvent(this, "related-changed", { value: undefined });
@@ -173,9 +186,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
:host {
display: inline-block;
position: relative;
}
:host([narrow]) {
position: static;
--mdc-menu-min-width: 250px;
}
ha-area-picker,
ha-device-picker,
@@ -185,8 +196,15 @@ export class HaRelatedFilterButtonMenu extends LitElement {
padding: 4px 16px;
box-sizing: border-box;
}
ha-area-picker {
padding-top: 16px;
}
ha-entity-picker {
padding-bottom: 16px;
}
:host([narrow]) ha-area-picker,
:host([narrow]) ha-device-picker {
:host([narrow]) ha-device-picker,
:host([narrow]) ha-entity-picker {
width: 100%;
}
`;

View File

@@ -1,11 +1,11 @@
import { mdiCalendar } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { formatDateNumeric } from "../common/datetime/format_date";
import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
import "./ha-svg-icon";
import "./ha-textfield";
const loadDatePickerDialog = () => import("./ha-dialog-date-picker");
@@ -38,17 +38,17 @@ export class HaDateInput extends LitElement {
@property() public label?: string;
render() {
return html`<paper-input
return html`<ha-textfield
.label=${this.label}
.disabled=${this.disabled}
no-label-float
iconTrailing="calendar"
@click=${this._openDialog}
.value=${this.value
? formatDateNumeric(new Date(this.value), this.locale)
: ""}
>
<ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon>
</paper-input>`;
<ha-svg-icon slot="trailingIcon" .path=${mdiCalendar}></ha-svg-icon>
</ha-textfield>`;
}
private _openDialog() {
@@ -73,9 +73,6 @@ export class HaDateInput extends LitElement {
static get styles(): CSSResultGroup {
return css`
paper-input {
width: 110px;
}
ha-svg-icon {
color: var(--secondary-text-color);
}

View File

@@ -11,7 +11,8 @@ import {
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-button-menu";
import { HaCheckListItem } from "../ha-check-list-item";
import "../ha-check-list-item";
import type { HaCheckListItem } from "../ha-check-list-item";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-formfield";

View File

@@ -32,7 +32,12 @@ export class HaForm extends LitElement implements HaFormElement {
@property() public computeError?: (schema: HaFormSchema, error) => string;
@property() public computeLabel?: (schema: HaFormSchema) => string;
@property() public computeLabel?: (
schema: HaFormSchema,
data?: HaFormDataContainer
) => string;
@property() public computeHelper?: (schema: HaFormSchema) => string;
public focus() {
const root = this.shadowRoot?.querySelector(".root");
@@ -71,6 +76,7 @@ export class HaForm extends LitElement implements HaFormElement {
: ""}
${this.schema.map((item) => {
const error = getValue(this.error, item);
return html`
${error
? html`
@@ -85,14 +91,15 @@ export class HaForm extends LitElement implements HaFormElement {
.hass=${this.hass}
.selector=${item.selector}
.value=${getValue(this.data, item)}
.label=${this._computeLabel(item)}
.label=${this._computeLabel(item, this.data)}
.disabled=${this.disabled}
.required=${item.required}
.helper=${this._computeHelper(item)}
.required=${item.required || false}
></ha-selector>`
: dynamicElement(`ha-form-${item.type}`, {
schema: item,
data: getValue(this.data, item),
label: this._computeLabel(item),
label: this._computeLabel(item, this.data),
disabled: this.disabled,
})}
`;
@@ -107,6 +114,7 @@ export class HaForm extends LitElement implements HaFormElement {
root.addEventListener("value-changed", (ev) => {
ev.stopPropagation();
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
fireEvent(this, "value-changed", {
value: { ...this.data, [schema.name]: ev.detail.value },
});
@@ -114,14 +122,18 @@ export class HaForm extends LitElement implements HaFormElement {
return root;
}
private _computeLabel(schema: HaFormSchema) {
private _computeLabel(schema: HaFormSchema, data: HaFormDataContainer) {
return this.computeLabel
? this.computeLabel(schema)
? this.computeLabel(schema, data)
: schema
? schema.name
: "";
}
private _computeHelper(schema: HaFormSchema) {
return this.computeHelper ? this.computeHelper(schema) : "";
}
private _computeError(error, schema: HaFormSchema | HaFormSchema[]) {
return this.computeError ? this.computeError(error, schema) : error;
}

View File

@@ -43,6 +43,8 @@ class HaHLSPlayer extends LitElement {
@state() private _error?: string;
@state() private _errorIsFatal = false;
private _hlsPolyfillInstance?: HlsLite;
private _exoPlayer = false;
@@ -53,6 +55,7 @@ class HaHLSPlayer extends LitElement {
super.connectedCallback();
HaHLSPlayer.streamCount += 1;
if (this.hasUpdated) {
this._resetError();
this._startHls();
}
}
@@ -64,16 +67,23 @@ class HaHLSPlayer extends LitElement {
}
protected render(): TemplateResult {
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
return html`
<video
?autoplay=${this.autoPlay}
.muted=${this.muted}
?playsinline=${this.playsInline}
?controls=${this.controls}
></video>
${this._error
? html`<ha-alert
alert-type="error"
class=${this._errorIsFatal ? "fatal" : "retry"}
>
${this._error}
</ha-alert>`
: ""}
${!this._errorIsFatal
? html`<video
?autoplay=${this.autoPlay}
.muted=${this.muted}
?playsinline=${this.playsInline}
?controls=${this.controls}
></video>`
: ""}
`;
}
@@ -87,12 +97,11 @@ class HaHLSPlayer extends LitElement {
}
this._cleanUp();
this._resetError();
this._startHls();
}
private async _startHls(): Promise<void> {
this._error = undefined;
const masterPlaylistPromise = fetch(this.url);
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min"))
@@ -110,8 +119,8 @@ class HaHLSPlayer extends LitElement {
}
if (!hlsSupported) {
this._error = this.hass.localize(
"ui.components.media-browser.video_not_supported"
this._setFatalError(
this.hass.localize("ui.components.media-browser.video_not_supported")
);
return;
}
@@ -219,9 +228,16 @@ class HaHLSPlayer extends LitElement {
this._hlsPolyfillInstance = hls;
hls.attachMedia(videoEl);
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
this._resetError();
hls.loadSource(url);
});
hls.on(Hls.Events.ERROR, (_, data: any) => {
hls.on(Hls.Events.FRAG_LOADED, (_event, _data: any) => {
this._resetError();
});
hls.on(Hls.Events.ERROR, (_event, data: any) => {
// Some errors are recovered automatically by the hls player itself, and the others handled
// in this function require special actions to recover. Errors retried in this function
// are done with backoff to not cause unecessary failures.
if (!data.fatal) {
return;
}
@@ -241,22 +257,22 @@ class HaHLSPlayer extends LitElement {
error += " (" + data.response.code + ")";
}
}
this._error = error;
return;
this._setRetryableError(error);
break;
}
case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT:
this._error = "Timeout while starting stream";
return;
this._setRetryableError("Timeout while starting stream");
break;
default:
this._error = "Unknown stream network error (" + data.details + ")";
return;
this._setRetryableError("Stream network error");
break;
}
this._error = "Error with media stream contents (" + data.details + ")";
hls.startLoad();
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
this._error = "Error with media stream contents (" + data.details + ")";
this._setRetryableError("Error with media stream contents");
hls.recoverMediaError();
} else {
this._error =
"Unknown error with stream (" + data.type + ", " + data.details + ")";
this._setFatalError("Error playing stream");
}
});
}
@@ -284,6 +300,21 @@ class HaHLSPlayer extends LitElement {
}
}
private _resetError() {
this._error = undefined;
this._errorIsFatal = false;
}
private _setFatalError(errorMessage: string) {
this._error = errorMessage;
this._errorIsFatal = true;
}
private _setRetryableError(errorMessage: string) {
this._error = errorMessage;
this._errorIsFatal = false;
}
static get styles(): CSSResultGroup {
return css`
:host,
@@ -296,10 +327,14 @@ class HaHLSPlayer extends LitElement {
max-height: var(--video-max-height, calc(100vh - 97px));
}
ha-alert {
.fatal {
display: block;
padding: 100px 16px;
}
.retry {
display: block;
}
`;
}
}

View File

@@ -1,28 +0,0 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import { PolymerElement } from "@polymer/polymer";
import { Constructor } from "../types";
const paperDropdownClass = customElements.get(
"paper-dropdown-menu"
) as Constructor<PolymerElement>;
// patches paper drop down to properly support RTL - https://github.com/PolymerElements/paper-dropdown-menu/issues/183
export class HaPaperDropdownClass extends paperDropdownClass {
public ready() {
super.ready();
// wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => {
if (window.getComputedStyle(this).direction === "rtl") {
this.style.textAlign = "right";
}
}, 100);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-paper-dropdown-menu": HaPaperDropdownClass;
}
}
customElements.define("ha-paper-dropdown-menu", HaPaperDropdownClass);

View File

@@ -1,5 +1,4 @@
import { mdiImagePlus } from "@mdi/js";
import "@polymer/paper-input/paper-input-container";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";

View File

@@ -0,0 +1,39 @@
import "../ha-icon-picker";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { HomeAssistant } from "../../types";
import { IconSelector } from "../../data/selector";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("ha-selector-icon")
export class HaIconSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: IconSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
return html`
<ha-icon-picker
.label=${this.label}
.value=${this.value}
@value-changed=${this._valueChanged}
></ha-icon-picker>
`;
}
private _valueChanged(ev: CustomEvent) {
fireEvent(this, "value-changed", { value: ev.detail.value });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-icon": HaIconSelector;
}
}

View File

@@ -2,7 +2,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { SelectSelector } from "../../data/selector";
import { SelectOption, SelectSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "@material/mwc-select/mwc-select";
import "@material/mwc-list/mwc-list-item";
@@ -17,6 +17,8 @@ export class HaSelectSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
@@ -25,15 +27,17 @@ export class HaSelectSelector extends LitElement {
naturalMenuWidth
.label=${this.label}
.value=${this.value}
.helper=${this.helper}
.disabled=${this.disabled}
@closed=${stopPropagation}
@selected=${this._valueChanged}
>
${this.selector.select.options.map(
(item: string) => html`
<mwc-list-item .value=${item}>${item}</mwc-list-item>
`
)}
${this.selector.select.options.map((item: string | SelectOption) => {
const value = typeof item === "object" ? item.value : item;
const label = typeof item === "object" ? item.label : item;
return html`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
})}
</mwc-select>`;
}

View File

@@ -17,6 +17,7 @@ import "./ha-selector-select";
import "./ha-selector-target";
import "./ha-selector-text";
import "./ha-selector-time";
import "./ha-selector-icon";
@customElement("ha-selector")
export class HaSelector extends LitElement {
@@ -28,6 +29,8 @@ export class HaSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property() public placeholder?: any;
@property({ type: Boolean }) public disabled = false;
@@ -52,6 +55,7 @@ export class HaSelector extends LitElement {
placeholder: this.placeholder,
disabled: this.disabled,
required: this.required,
helper: this.helper,
id: "selector",
})}
`;

View File

@@ -489,9 +489,6 @@ export class HaServiceControl extends LitElement {
margin: var(--service-control-padding, 0 16px);
padding: 16px 0;
}
:host(:not([narrow])) ha-settings-row paper-input {
width: 60%;
}
:host(:not([narrow])) ha-settings-row ha-selector {
width: 60%;
}

View File

@@ -68,6 +68,14 @@ export class HaTextField extends TextFieldBase {
:host([no-spinner]) input[type="number"] {
-moz-appearance: textfield;
}
.mdc-text-field__ripple {
overflow: hidden;
}
.mdc-text-field {
overflow: var(--text-field-overflow);
}
`,
];
}

View File

@@ -0,0 +1,230 @@
import "@material/mwc-select";
import "@material/mwc-list/mwc-list-item";
import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { fetchCloudStatus, updateCloudPref } from "../../data/cloud";
import {
CloudTTSInfo,
getCloudTTSInfo,
getCloudTtsLanguages,
getCloudTtsSupportedGenders,
} from "../../data/cloud/tts";
import { MediaPlayerBrowseAction } from "../../data/media-player";
import { HomeAssistant } from "../../types";
import "../ha-textarea";
import { buttonLinkStyle } from "../../resources/styles";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { LocalStorage } from "../../common/decorators/local-storage";
@customElement("ha-browse-media-tts")
class BrowseMediaTTS extends LitElement {
@property() public hass!: HomeAssistant;
@property() public item;
@property() public action!: MediaPlayerBrowseAction;
@state() private _cloudDefaultOptions?: [string, string];
@state() private _cloudOptions?: [string, string];
@state() private _cloudTTSInfo?: CloudTTSInfo;
@LocalStorage("cloudTtsTryMessage", false, false) private _message!: string;
protected render() {
return html`
<ha-textarea
autogrow
.label=${this.hass.localize("ui.panel.media-browser.tts.message")}
.value=${this._message ||
this.hass.localize("ui.panel.media-browser.tts.example_message", {
name: this.hass.user?.name || "",
})}
>
</ha-textarea>
${this._cloudDefaultOptions ? this._renderCloudOptions() : ""}
<div class="actions">
${this._cloudDefaultOptions &&
(this._cloudDefaultOptions![0] !== this._cloudOptions![0] ||
this._cloudDefaultOptions![1] !== this._cloudOptions![1])
? html`
<button class="link" @click=${this._storeDefaults}>
${this.hass.localize(
"ui.panel.media-browser.tts.set_as_default"
)}
</button>
`
: html`<span></span>`}
<mwc-button raised label="Say" @click=${this._ttsClicked}></mwc-button>
</div>
`;
}
private _renderCloudOptions() {
const languages = this.getLanguages(this._cloudTTSInfo);
const selectedVoice = this._cloudOptions!;
const genders = this.getSupportedGenders(
selectedVoice[0],
this._cloudTTSInfo,
this.hass.localize
);
return html`
<div class="cloud-options">
<mwc-select
fixedMenuPosition
naturalMenuWidth
.label=${this.hass.localize("ui.panel.media-browser.tts.language")}
.value=${selectedVoice[0]}
@selected=${this._handleLanguageChange}
>
${languages.map(
([key, label]) =>
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
)}
</mwc-select>
<mwc-select
fixedMenuPosition
naturalMenuWidth
.label=${this.hass.localize("ui.panel.media-browser.tts.gender")}
.value=${selectedVoice[1]}
@selected=${this._handleGenderChange}
>
${genders.map(
([key, label]) =>
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
)}
</mwc-select>
</div>
`;
}
protected override willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("message")) {
return;
}
// Re-rendering can reset message because textarea content is newer than local storage.
// But we don't want to write every keystroke to local storage.
// So instead we just do it when we're going to render.
const message = this.shadowRoot!.querySelector("ha-textarea")?.value;
if (message !== undefined && message !== this._message) {
this._message = message;
}
}
async _handleLanguageChange(ev) {
if (ev.target.value === this._cloudOptions![0]) {
return;
}
this._cloudOptions = [ev.target.value, this._cloudOptions![1]];
}
async _handleGenderChange(ev) {
if (ev.target.value === this._cloudOptions![1]) {
return;
}
this._cloudOptions = [this._cloudOptions![0], ev.target.value];
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("item")) {
if (this.isCloudItem && !this._cloudTTSInfo) {
getCloudTTSInfo(this.hass).then((info) => {
this._cloudTTSInfo = info;
});
fetchCloudStatus(this.hass).then((status) => {
if (status.logged_in) {
this._cloudDefaultOptions = status.prefs.tts_default_voice;
this._cloudOptions = { ...this._cloudDefaultOptions };
}
});
}
}
}
private getLanguages = memoizeOne(getCloudTtsLanguages);
private getSupportedGenders = memoizeOne(getCloudTtsSupportedGenders);
private get isCloudItem(): boolean {
return this.item.media_content_id === "media-source://tts/cloud";
}
private async _ttsClicked(): Promise<void> {
const message = this.shadowRoot!.querySelector("ha-textarea")!.value;
this._message = message;
const item = { ...this.item };
const query = new URLSearchParams();
query.append("message", message);
if (this._cloudOptions) {
query.append("language", this._cloudOptions[0]);
query.append("gender", this._cloudOptions[1]);
}
item.media_content_id += `?${query.toString()}`;
item.can_play = true;
fireEvent(this, "media-picked", { item });
}
private async _storeDefaults() {
const oldDefaults = this._cloudDefaultOptions!;
this._cloudDefaultOptions = [...this._cloudOptions!];
try {
await updateCloudPref(this.hass, {
tts_default_voice: this._cloudDefaultOptions,
});
} catch (err: any) {
this._cloudDefaultOptions = oldDefaults;
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.media-browser.tts.faild_to_store_defaults",
{ error: err.message || err }
),
});
}
}
static override styles = [
buttonLinkStyle,
css`
:host {
margin: 16px auto;
padding: 0 8px;
display: flex;
flex-direction: column;
max-width: 400px;
}
.cloud-options {
margin-top: 16px;
display: flex;
justify-content: space-between;
}
.cloud-options mwc-select {
width: 48%;
}
.actions {
display: flex;
justify-content: space-between;
margin-top: 16px;
}
button.link {
color: var(--primary-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-browse-media-tts": BrowseMediaTTS;
}
}

View File

@@ -1,8 +1,7 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiPlay, mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
@@ -49,11 +48,20 @@ import "../ha-icon-button";
import "../ha-svg-icon";
import "../ha-fab";
import { browseLocalMediaPlayer } from "../../data/media_source";
import { isTTSMediaSource } from "../../data/tts";
import "./ha-browse-media-tts";
declare global {
interface HASSDomEvents {
"media-picked": MediaPickedEvent;
"media-browsed": { ids: MediaPlayerItemId[]; current?: MediaPlayerItem };
"media-browsed": {
// Items of the new browse stack
ids: MediaPlayerItemId[];
// Current fetched item for this browse stack
current?: MediaPlayerItem;
// If the new stack should replace the old stack
replace?: boolean;
};
}
}
@@ -246,160 +254,160 @@ export class HaMediaPlayerBrowse extends LitElement {
${this._renderError(this._error)}
</div>
`
: currentItem.children?.length
? childrenMediaClass.layout === "grid"
? html`
<div
class="children ${classMap({
portrait:
childrenMediaClass.thumbnail_ratio === "portrait",
})}"
>
${currentItem.children.map(
(child) => html`
<div
class="child"
.item=${child}
@click=${this._childClicked}
>
<ha-card outlined>
<div class="thumbnail">
${child.thumbnail
? html`
<div
class="${[
"app",
"directory",
].includes(child.media_class)
? "centered-image"
: ""} image lazythumbnail"
data-src=${child.thumbnail}
></div>
`
: html`
<div class="icon-holder image">
<ha-svg-icon
class="folder"
.path=${MediaClassBrowserSettings[
child.media_class === "directory"
? child.children_media_class ||
child.media_class
: child.media_class
].icon}
></ha-svg-icon>
</div>
`}
${child.can_play
? html`
<ha-icon-button
class="play ${classMap({
can_expand: child.can_expand,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
`
: ""}
</div>
<div class="title">
${child.title}
<paper-tooltip
fitToVisibleBounds
position="top"
offset="4"
>${child.title}</paper-tooltip
>
</div>
</ha-card>
</div>
`
)}
</div>
`
: html`
<mwc-list>
${currentItem.children.map(
(child) => html`
<mwc-list-item
@click=${this._childClicked}
.item=${child}
.graphic=${mediaClass.show_list_images
? "medium"
: "avatar"}
dir=${computeRTLDirection(this.hass)}
>
<div
class=${classMap({
graphic: true,
lazythumbnail:
mediaClass.show_list_images === true,
})}
data-src=${ifDefined(
mediaClass.show_list_images && child.thumbnail
? child.thumbnail
: undefined
)}
slot="graphic"
>
<ha-icon-button
class="play ${classMap({
show:
!mediaClass.show_list_images ||
!child.thumbnail,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
</div>
<span class="title">${child.title}</span>
</mwc-list-item>
<li divider role="separator"></li>
`
)}
</mwc-list>
`
: html`
: isTTSMediaSource(currentItem.media_content_id)
? html`
<ha-browse-media-tts
.item=${currentItem}
.hass=${this.hass}
.action=${this.action}
></ha-browse-media-tts>
`
: !currentItem.children?.length
? html`
<div class="container no-items">
${this.hass.localize(
"ui.components.media-browser.no_items"
)}
<br />
${currentItem.media_content_id ===
"media-source://media_source/local/."
? html`<br />${this.hass.localize(
"ui.components.media-browser.learn_adding_local_media",
"documentation",
html`<a
href=${documentationUrl(
this.hass,
"/more-info/local-media/add-media"
? html`
<div class="highlight-add-button">
<span>
<ha-svg-icon
.path=${mdiArrowUpRight}
></ha-svg-icon>
</span>
<span>
${this.hass.localize(
"ui.components.media-browser.file_management.highlight_button"
)}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.components.media-browser.documentation"
)}</a
>`
)}
<br />
${this.hass.localize(
"ui.components.media-browser.local_media_files"
)}`
: ""}
</span>
</div>
`
: this.hass.localize(
"ui.components.media-browser.no_items"
)}
</div>
`
: childrenMediaClass.layout === "grid"
? html`
<div
class="children ${classMap({
portrait:
childrenMediaClass.thumbnail_ratio === "portrait",
})}"
>
${currentItem.children.map(
(child) => html`
<div
class="child"
.item=${child}
@click=${this._childClicked}
>
<ha-card outlined>
<div class="thumbnail">
${child.thumbnail
? html`
<div
class="${["app", "directory"].includes(
child.media_class
)
? "centered-image"
: ""} image lazythumbnail"
data-src=${child.thumbnail}
></div>
`
: html`
<div class="icon-holder image">
<ha-svg-icon
class="folder"
.path=${MediaClassBrowserSettings[
child.media_class === "directory"
? child.children_media_class ||
child.media_class
: child.media_class
].icon}
></ha-svg-icon>
</div>
`}
${child.can_play
? html`
<ha-icon-button
class="play ${classMap({
can_expand: child.can_expand,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
`
: ""}
</div>
<div class="title">
${child.title}
<paper-tooltip
fitToVisibleBounds
position="top"
offset="4"
>${child.title}</paper-tooltip
>
</div>
</ha-card>
</div>
`
)}
</div>
`
: html`
<mwc-list>
${currentItem.children.map(
(child) => html`
<mwc-list-item
@click=${this._childClicked}
.item=${child}
.graphic=${mediaClass.show_list_images
? "medium"
: "avatar"}
dir=${computeRTLDirection(this.hass)}
>
<div
class=${classMap({
graphic: true,
lazythumbnail:
mediaClass.show_list_images === true,
})}
data-src=${ifDefined(
mediaClass.show_list_images && child.thumbnail
? child.thumbnail
: undefined
)}
slot="graphic"
>
<ha-icon-button
class="play ${classMap({
show:
!mediaClass.show_list_images ||
!child.thumbnail,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
</div>
<span class="title">${child.title}</span>
</mwc-list-item>
<li divider role="separator"></li>
`
)}
</mwc-list>
`
}
</div>
</div>
@@ -425,8 +433,8 @@ export class HaMediaPlayerBrowse extends LitElement {
if (changedProps.has("entityId")) {
this._setError(undefined);
}
if (!changedProps.has("navigateIds")) {
} else if (!changedProps.has("navigateIds")) {
// Neither entity ID or navigateIDs changed, nothing to fetch
return;
}
@@ -435,6 +443,7 @@ export class HaMediaPlayerBrowse extends LitElement {
const oldNavigateIds = changedProps.get("navigateIds") as
| this["navigateIds"]
| undefined;
const navigateIds = this.navigateIds;
// We're navigating. Reset the shizzle.
this._content?.scrollTo(0, 0);
@@ -443,11 +452,9 @@ export class HaMediaPlayerBrowse extends LitElement {
const oldParentItem = this._parentItem;
this._currentItem = undefined;
this._parentItem = undefined;
const currentId = this.navigateIds[this.navigateIds.length - 1];
const currentId = navigateIds[navigateIds.length - 1];
const parentId =
this.navigateIds.length > 1
? this.navigateIds[this.navigateIds.length - 2]
: undefined;
navigateIds.length > 1 ? navigateIds[navigateIds.length - 2] : undefined;
let currentProm: Promise<MediaPlayerItem> | undefined;
let parentProm: Promise<MediaPlayerItem> | undefined;
@@ -456,9 +463,9 @@ export class HaMediaPlayerBrowse extends LitElement {
if (
// Check if we navigated to a child
oldNavigateIds &&
this.navigateIds.length > oldNavigateIds.length &&
navigateIds.length === oldNavigateIds.length + 1 &&
oldNavigateIds.every((oldVal, idx) => {
const curVal = this.navigateIds[idx];
const curVal = navigateIds[idx];
return (
curVal.media_content_id === oldVal.media_content_id &&
curVal.media_content_type === oldVal.media_content_type
@@ -469,8 +476,8 @@ export class HaMediaPlayerBrowse extends LitElement {
} else if (
// Check if we navigated to a parent
oldNavigateIds &&
this.navigateIds.length < oldNavigateIds.length &&
this.navigateIds.every((curVal, idx) => {
navigateIds.length === oldNavigateIds.length - 1 &&
navigateIds.every((curVal, idx) => {
const oldVal = oldNavigateIds[idx];
return (
curVal.media_content_id === oldVal.media_content_id &&
@@ -493,11 +500,33 @@ export class HaMediaPlayerBrowse extends LitElement {
(item) => {
this._currentItem = item;
fireEvent(this, "media-browsed", {
ids: this.navigateIds,
ids: navigateIds,
current: item,
});
},
(err) => this._setError(err)
(err) => {
// When we change entity ID, we will first try to see if the new entity is
// able to resolve the new path. If that results in an error, browse the root.
const isNewEntityWithSamePath =
oldNavigateIds &&
changedProps.has("entityId") &&
navigateIds.length === oldNavigateIds.length &&
oldNavigateIds.every(
(oldItem, idx) =>
navigateIds[idx].media_content_id === oldItem.media_content_id &&
navigateIds[idx].media_content_type === oldItem.media_content_type
);
if (isNewEntityWithSamePath) {
fireEvent(this, "media-browsed", {
ids: [
{ media_content_id: undefined, media_content_type: undefined },
],
replace: true,
});
} else {
this._setError(err);
}
}
);
// Fetch parent
if (!parentProm && parentId !== undefined) {
@@ -736,6 +765,18 @@ export class HaMediaPlayerBrowse extends LitElement {
padding-left: 32px;
}
.highlight-add-button {
display: flex;
flex-direction: row-reverse;
margin-right: 48px;
}
.highlight-add-button ha-svg-icon {
position: relative;
top: -0.5em;
margin-left: 8px;
}
.content {
overflow-y: auto;
box-sizing: border-box;

View File

@@ -186,10 +186,3 @@ export const updateCloudAlexaEntityConfig = (
entity_id: entityId,
...values,
});
export interface CloudTTSInfo {
languages: Array<[string, string]>;
}
export const getCloudTTSInfo = (hass: HomeAssistant) =>
hass.callWS<CloudTTSInfo>({ type: "cloud/tts/info" });

70
src/data/cloud/tts.ts Normal file
View File

@@ -0,0 +1,70 @@
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { LocalizeFunc } from "../../common/translations/localize";
import { translationMetadata } from "../../resources/translations-metadata";
import { HomeAssistant } from "../../types";
export interface CloudTTSInfo {
languages: Array<[string, string]>;
}
export const getCloudTTSInfo = (hass: HomeAssistant) =>
hass.callWS<CloudTTSInfo>({ type: "cloud/tts/info" });
export const getCloudTtsLanguages = (info?: CloudTTSInfo) => {
const languages: Array<[string, string]> = [];
if (!info) {
return languages;
}
const seen = new Set<string>();
for (const [lang] of info.languages) {
if (seen.has(lang)) {
continue;
}
seen.add(lang);
let label = lang;
if (lang in translationMetadata.translations) {
label = translationMetadata.translations[lang].nativeName;
} else {
const [langFamily, dialect] = lang.split("-");
if (langFamily in translationMetadata.translations) {
label = `${translationMetadata.translations[langFamily].nativeName}`;
if (langFamily.toLowerCase() !== dialect.toLowerCase()) {
label += ` (${dialect})`;
}
}
}
languages.push([lang, label]);
}
return languages.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1]));
};
export const getCloudTtsSupportedGenders = (
language: string,
info: CloudTTSInfo | undefined,
localize: LocalizeFunc
) => {
const genders: Array<[string, string]> = [];
if (!info) {
return genders;
}
for (const [curLang, gender] of info.languages) {
if (curLang === language) {
genders.push([
gender,
localize(`ui.panel.media-browser.tts.gender_${gender}`) ||
localize(`ui.panel.config.cloud.account.tts.${gender}`) ||
gender,
]);
}
}
return genders.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1]));
};

View File

@@ -261,8 +261,10 @@ export const computeMediaControls = (
});
}
const assumedState = stateObj.attributes.assumed_state === true;
if (
(state === "playing" || state === "paused") &&
(state === "playing" || state === "paused" || assumedState) &&
supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK)
) {
buttons.push({
@@ -272,14 +274,15 @@ export const computeMediaControls = (
}
if (
(state === "playing" &&
!assumedState &&
((state === "playing" &&
(supportsFeature(stateObj, SUPPORT_PAUSE) ||
supportsFeature(stateObj, SUPPORT_STOP))) ||
((state === "paused" || state === "idle") &&
supportsFeature(stateObj, SUPPORT_PLAY)) ||
(state === "on" &&
(supportsFeature(stateObj, SUPPORT_PLAY) ||
supportsFeature(stateObj, SUPPORT_PAUSE)))
((state === "paused" || state === "idle") &&
supportsFeature(stateObj, SUPPORT_PLAY)) ||
(state === "on" &&
(supportsFeature(stateObj, SUPPORT_PLAY) ||
supportsFeature(stateObj, SUPPORT_PAUSE))))
) {
buttons.push({
icon:
@@ -299,8 +302,29 @@ export const computeMediaControls = (
});
}
if (assumedState && supportsFeature(stateObj, SUPPORT_PLAY)) {
buttons.push({
icon: mdiPlay,
action: "media_play",
});
}
if (assumedState && supportsFeature(stateObj, SUPPORT_PAUSE)) {
buttons.push({
icon: mdiPause,
action: "media_pause",
});
}
if (assumedState && supportsFeature(stateObj, SUPPORT_STOP)) {
buttons.push({
icon: mdiStop,
action: "media_stop",
});
}
if (
(state === "playing" || state === "paused") &&
(state === "playing" || state === "paused" || assumedState) &&
supportsFeature(stateObj, SUPPORT_NEXT_TRACK)
) {
buttons.push({

View File

@@ -22,6 +22,7 @@ export interface MQTTEntityDebugInfo {
entity_id: string;
discovery_data: MQTTDiscoveryDebugInfo;
subscriptions: MQTTTopicDebugInfo[];
transmitted: MQTTTopicDebugInfo[];
}
export interface MQTTTriggerDebugInfo {

View File

@@ -70,10 +70,15 @@ export interface DelayAction {
delay: number | Partial<DelayActionParts> | string;
}
export interface SceneAction {
export interface ServiceSceneAction extends ServiceAction {
service: "scene.turn_on";
metadata: Record<string, any>;
}
export interface LegacySceneAction {
alias?: string;
scene: string;
}
export type SceneAction = ServiceSceneAction | LegacySceneAction;
export interface WaitAction {
alias?: string;
@@ -153,7 +158,8 @@ export interface ActionTypes {
check_condition: Condition;
fire_event: EventAction;
device_action: DeviceAction;
activate_scene: SceneAction;
legacy_activate_scene: LegacySceneAction;
activate_scene: ServiceSceneAction;
repeat: RepeatAction;
choose: ChooseAction;
wait_for_trigger: WaitForTriggerAction;
@@ -218,7 +224,7 @@ export const getActionType = (action: Action): ActionType => {
return "device_action";
}
if ("scene" in action) {
return "activate_scene";
return "legacy_activate_scene";
}
if ("repeat" in action) {
return "repeat";
@@ -233,6 +239,14 @@ export const getActionType = (action: Action): ActionType => {
return "variables";
}
if ("service" in action) {
if ("metadata" in action) {
if (
(action as ServiceAction).service === "scene.turn_on" &&
!Array.isArray((action as ServiceAction)?.target?.entity_id)
) {
return "activate_scene";
}
}
return "service";
}
return "unknown";

View File

@@ -11,7 +11,8 @@ import {
DelayAction,
EventAction,
getActionType,
SceneAction,
LegacySceneAction,
ServiceSceneAction,
VariablesAction,
WaitForTriggerAction,
} from "./script";
@@ -102,14 +103,22 @@ export const describeAction = <T extends ActionType>(
return `Delay ${duration}`;
}
if (actionType === "activate_scene") {
const config = action as SceneAction;
if (actionType === "legacy_activate_scene") {
const config = action as LegacySceneAction;
const sceneStateObj = hass.states[config.scene];
return `Activate scene ${
sceneStateObj ? computeStateName(sceneStateObj) : config.scene
}`;
}
if (actionType === "activate_scene") {
const config = action as ServiceSceneAction;
const sceneStateObj = hass.states[config.target!.entity_id as string];
return `Activate scene ${
sceneStateObj ? computeStateName(sceneStateObj) : config.target!.entity_id
}`;
}
if (actionType === "wait_for_trigger") {
const config = action as WaitForTriggerAction;
return `Wait for ${ensureArray(config.wait_for_trigger)

View File

@@ -12,7 +12,8 @@ export type Selector =
| ActionSelector
| StringSelector
| ObjectSelector
| SelectSelector;
| SelectSelector
| IconSelector;
export interface EntitySelector {
entity: {
@@ -133,8 +134,18 @@ export interface ObjectSelector {
object: {};
}
export interface SelectOption {
value: string;
label: string;
}
export interface SelectSelector {
select: {
options: string[];
options: string[] | SelectOption[];
};
}
export interface IconSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
icon: {};
}

View File

@@ -10,3 +10,11 @@ export const convertTextToSpeech = (
options?: Record<string, unknown>;
}
) => hass.callApi<{ url: string; path: string }>("POST", "tts_get_url", data);
const TTS_MEDIA_SOURCE_PREFIX = "media-source://tts/";
export const isTTSMediaSource = (mediaContentId: string) =>
mediaContentId.startsWith(TTS_MEDIA_SOURCE_PREFIX);
export const getProviderFromTTSMediaSource = (mediaContentId: string) =>
mediaContentId.substring(TTS_MEDIA_SOURCE_PREFIX.length);

View File

@@ -126,6 +126,7 @@ export interface ZWaveJSNodeStatus {
is_routing: boolean | null;
zwave_plus_version: number | null;
highest_security_class: SecurityClass | null;
is_controller_node: boolean;
}
export interface ZwaveJSNodeMetadata {

View File

@@ -83,6 +83,7 @@ class DialogConfigEntrySystemOptions extends LitElement {
.checked=${!this._disableNewEntities}
@change=${this._disableNewEntitiesChanged}
.disabled=${this._submitting}
dialogInitialFocus
></ha-switch>
</ha-formfield>
${this._allowUpdatePolling()

View File

@@ -1,23 +1,27 @@
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import Fuse from "fuse.js";
import {
css,
CSSResultGroup,
html,
LitElement,
TemplateResult,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import { navigate } from "../../common/navigate";
import "../../common/search/search-input";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { LocalizeFunc } from "../../common/translations/localize";
import "../../components/ha-icon-next";
import { getConfigEntries } from "../../data/config_entries";
import { domainToName } from "../../data/integration";
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
import { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import { documentationUrl } from "../../util/documentation-url";
@@ -26,6 +30,7 @@ import { configFlowContentStyles } from "./styles";
interface HandlerObj {
name: string;
slug: string;
is_add?: boolean;
}
declare global {
@@ -77,6 +82,17 @@ class StepFlowPickHandler extends LitElement {
protected render(): TemplateResult {
const handlers = this._getHandlers();
const addDeviceRows: HandlerObj[] = ["zha", "zwave_js"]
.filter((domain) => isComponentLoaded(this.hass, domain))
.map((domain) => ({
name: this.hass.localize(
`ui.panel.config.integrations.add_${domain}_device`
),
slug: domain,
is_add: true,
}))
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name));
return html`
<h2>${this.hass.localize("ui.panel.config.integrations.new")}</h2>
<search-input
@@ -87,37 +103,20 @@ class StepFlowPickHandler extends LitElement {
.label=${this.hass.localize("ui.panel.config.integrations.search")}
@keypress=${this._maybeSubmit}
></search-input>
<div
<mwc-list
style=${styleMap({
width: `${this._width}px`,
height: `${this._height}px`,
})}
>
${addDeviceRows.length
? html`
${addDeviceRows.map((handler) => this._renderRow(handler))}
<li divider padded class="divider" role="separator"></li>
`
: ""}
${handlers.length
? handlers.map(
(handler: HandlerObj) =>
html`
<paper-icon-item
@click=${this._handlerPicked}
.handler=${handler}
>
<img
slot="item-icon"
loading="lazy"
src=${brandsUrl({
domain: handler.slug,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
/>
<paper-item-body> ${handler.name} </paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-icon-item>
`
)
? handlers.map((handler) => this._renderRow(handler))
: html`
<p>
${this.hass.localize(
@@ -140,15 +139,56 @@ class StepFlowPickHandler extends LitElement {
>.
</p>
`}
</div>
</mwc-list>
`;
}
private _renderRow(handler: HandlerObj) {
return html`
<mwc-list-item
graphic="medium"
.hasMeta=${!handler.is_add}
.handler=${handler}
@click=${this._handlerPicked}
>
<img
slot="graphic"
loading="lazy"
src=${brandsUrl({
domain: handler.slug,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
/>
<span>${handler.name}</span>
${handler.is_add ? "" : html`<ha-icon-next slot="meta"></ha-icon-next>`}
</mwc-list-item>
`;
}
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (this._filter === undefined && this.initialFilter !== undefined) {
this._filter = this.initialFilter;
}
super.willUpdate(changedProps);
if (this.initialFilter !== undefined && this._filter === "") {
this.initialFilter = undefined;
this._filter = "";
this._width = undefined;
this._height = undefined;
} else if (
this.hasUpdated &&
changedProps.has("_filter") &&
(!this._width || !this._height)
) {
// Store the width and height so that when we search, box doesn't jump
const boundingRect =
this.shadowRoot!.querySelector("mwc-list")!.getBoundingClientRect();
this._width = boundingRect.width;
this._height = boundingRect.height;
}
}
protected firstUpdated(changedProps) {
@@ -159,24 +199,6 @@ class StepFlowPickHandler extends LitElement {
);
}
protected updated(changedProps) {
super.updated(changedProps);
// Store the width and height so that when we search, box doesn't jump
const div = this.shadowRoot!.querySelector("div")!;
if (!this._width) {
const width = div.clientWidth;
if (width) {
this._width = width;
}
}
if (!this._height) {
const height = div.clientHeight;
if (height) {
this._height = height;
}
}
}
private _getHandlers() {
return this._filterHandlers(
this.handlers,
@@ -190,8 +212,31 @@ class StepFlowPickHandler extends LitElement {
}
private async _handlerPicked(ev) {
const handler: HandlerObj = ev.currentTarget.handler;
if (handler.is_add) {
if (handler.slug === "zwave_js") {
const entries = await getConfigEntries(this.hass);
const entry = entries.find((ent) => ent.domain === "zwave_js");
if (!entry) {
return;
}
showZWaveJSAddNodeDialog(this, {
entry_id: entry.entry_id,
});
} else if (handler.slug === "zha") {
navigate("/config/zha/add");
}
// This closes dialog.
fireEvent(this, "flow-update");
return;
}
fireEvent(this, "handler-picked", {
handler: ev.currentTarget.handler.slug,
handler: handler.slug,
});
}
@@ -219,27 +264,26 @@ class StepFlowPickHandler extends LitElement {
}
search-input {
display: block;
margin: -12px 16px 0;
margin: 16px 16px 0;
}
ha-icon-next {
margin-right: 8px;
}
div {
mwc-list {
overflow: auto;
max-height: 600px;
}
.divider {
border-bottom-color: var(--divider-color);
}
h2 {
padding-right: 66px;
}
@media all and (max-height: 900px) {
div {
mwc-list {
max-height: calc(100vh - 134px);
}
}
paper-icon-item {
cursor: pointer;
margin-bottom: 4px;
}
p {
text-align: center;
padding: 16px;

View File

@@ -615,10 +615,6 @@ class MoreInfoLight extends LitElement {
color: var(--secondary-text-color);
}
paper-item {
cursor: pointer;
}
hr {
border-color: var(--divider-color);
border-bottom: none;

View File

@@ -1,4 +1,4 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attributes";
@@ -66,14 +66,6 @@ class MoreInfoRemote extends LitElement {
activity: newVal,
});
}
static get styles(): CSSResultGroup {
return css`
paper-item {
cursor: pointer;
}
`;
}
}
declare global {

View File

@@ -241,9 +241,6 @@ class MoreInfoVacuum extends LitElement {
.status-subtitle {
color: var(--secondary-text-color);
}
paper-item {
cursor: pointer;
}
.flex-horizontal {
display: flex;
flex-direction: row;

View File

@@ -338,7 +338,9 @@ export class MoreInfoDialog extends LitElement {
flex-shrink: 0;
display: block;
}
.content {
outline: none;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-header-bar {
--mdc-theme-primary: var(--app-header-background-color);

View File

@@ -1,7 +1,11 @@
import { setPassiveTouchGestures } from "@polymer/polymer/lib/utils/settings";
import {
setPassiveTouchGestures,
setCancelSyntheticClickEvents,
} from "@polymer/polymer/lib/utils/settings";
import "../layouts/home-assistant";
import "../resources/ha-style";
import "../resources/roboto";
import "../util/legacy-support";
setPassiveTouchGestures(true);
setCancelSyntheticClickEvents(false);

View File

@@ -1,13 +1,10 @@
// Compat needs to be first import
import "../resources/compatibility";
import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings";
import "../auth/ha-authorize";
import "../resources/ha-style";
import "../resources/roboto";
import "../resources/safari-14-attachshadow-patch";
import "../resources/array.flat.polyfill";
/* polyfill for paper-dropdown */
setTimeout(
() => import("web-animations-js/web-animations-next-lite.min"),
2000
);
setCancelSyntheticClickEvents(false);

View File

@@ -1,5 +1,6 @@
// Compat needs to be first import
import "../resources/compatibility";
import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings";
import "../resources/safari-14-attachshadow-patch";
import { PolymerElement } from "@polymer/polymer";
@@ -15,6 +16,8 @@ import { createCustomPanelElement } from "../util/custom-panel/create-custom-pan
import { loadCustomPanel } from "../util/custom-panel/load-custom-panel";
import { setCustomPanelProperties } from "../util/custom-panel/set-custom-panel-properties";
setCancelSyntheticClickEvents(false);
declare global {
interface Window {
loadES5Adapter: () => Promise<unknown>;
@@ -47,7 +50,8 @@ function initialize(
) {
const style = document.createElement("style");
style.innerHTML = `body { margin:0; }
style.innerHTML = `
body { margin:0; }
@media (prefers-color-scheme: dark) {
body {
background-color: #111111;

View File

@@ -1,11 +1,14 @@
// Compat needs to be first import
import "../resources/compatibility";
import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings";
import "../onboarding/ha-onboarding";
import "../resources/ha-style";
import "../resources/roboto";
import "../resources/safari-14-attachshadow-patch";
import "../resources/array.flat.polyfill";
setCancelSyntheticClickEvents(false);
declare global {
interface Window {
stepsPromise: Promise<Response>;

View File

@@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button";
import { mdiFilterVariant } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
@@ -157,30 +156,31 @@ export class HaTabsSubpageDataTable extends LitElement {
: hiddenLabel;
const headerToolbar = html`<search-input
.hass=${this.hass}
.filter=${this.filter}
@value-changed=${this._handleSearchChange}
.label=${this.searchLabel ||
this.hass.localize("ui.components.data-table.search")}
>
</search-input>
<div class="filters">
${filterInfo
? html`<div class="active-filters">
${this.narrow
? html`<div>
<ha-svg-icon .path=${mdiFilterVariant}></ha-svg-icon>
<paper-tooltip animation-delay="0" position="left">
${filterInfo}
</paper-tooltip>
</div>`
: filterInfo}
<mwc-button @click=${this._clearFilter}>
${this.hass.localize("ui.components.data-table.clear")}
</mwc-button>
</div>`
: ""}<slot name="filter-menu"></slot>
</div>`;
.hass=${this.hass}
.filter=${this.filter}
.suffix=${!this.narrow}
@value-changed=${this._handleSearchChange}
.label=${this.searchLabel ||
this.hass.localize("ui.components.data-table.search")}
>
${!this.narrow
? html`<div
class="filters"
slot="suffix"
@click=${this._preventDefault}
>
${filterInfo
? html`<div class="active-filters">
${filterInfo}
<mwc-button @click=${this._clearFilter}>
${this.hass.localize("ui.components.data-table.clear")}
</mwc-button>
</div>`
: ""}
<slot name="filter-menu"></slot>
</div>`
: ""}
</search-input>`;
return html`
<hass-tabs-subpage
@@ -195,7 +195,16 @@ export class HaTabsSubpageDataTable extends LitElement {
.mainPage=${this.mainPage}
.supervisor=${this.supervisor}
>
<div slot="toolbar-icon"><slot name="toolbar-icon"></slot></div>
<div slot="toolbar-icon">
${this.narrow
? html`<div class="filter-menu">
${this.numHidden || this.activeFilters
? html`<span class="badge">${this.numHidden || "!"}</span>`
: ""}
<slot name="filter-menu"></slot>
</div>`
: ""}<slot name="toolbar-icon"></slot>
</div>
${this.narrow
? html`
<div slot="header">
@@ -233,6 +242,10 @@ export class HaTabsSubpageDataTable extends LitElement {
`;
}
private _preventDefault(ev) {
ev.preventDefault();
}
private _handleSearchChange(ev: CustomEvent) {
if (this.filter === ev.detail.value) {
return;
@@ -267,6 +280,12 @@ export class HaTabsSubpageDataTable extends LitElement {
align-items: center;
color: var(--secondary-text-color);
}
search-input {
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
--text-field-overflow: visible;
z-index: 5;
}
.table-header search-input {
display: block;
position: absolute;
@@ -276,16 +295,19 @@ export class HaTabsSubpageDataTable extends LitElement {
}
.search-toolbar search-input {
display: block;
width: 100%;
color: var(--secondary-text-color);
--mdc-text-field-fill-color: transparant;
--mdc-text-field-idle-line-color: var(--divider-color);
--mdc-ripple-color: transparant;
}
.filters {
--mdc-text-field-fill-color: var(--input-fill-color);
--mdc-text-field-idle-line-color: var(--input-idle-line-color);
--mdc-shape-small: 4px;
--text-field-overflow: initial;
display: flex;
justify-content: flex-end;
width: 100%;
margin-right: 8px;
color: var(--primary-text-color);
}
.active-filters {
color: var(--primary-text-color);
@@ -295,6 +317,8 @@ export class HaTabsSubpageDataTable extends LitElement {
padding: 2px 2px 2px 8px;
margin-left: 4px;
font-size: 14px;
width: max-content;
cursor: initial;
}
.active-filters ha-svg-icon {
color: var(--primary-color);
@@ -313,6 +337,24 @@ export class HaTabsSubpageDataTable extends LitElement {
left: 0;
content: "";
}
.badge {
min-width: 20px;
box-sizing: border-box;
border-radius: 50%;
font-weight: 400;
background-color: var(--primary-color);
line-height: 20px;
text-align: center;
padding: 0px 4px;
color: var(--text-primary-color);
position: absolute;
right: 0;
top: 4px;
font-size: 0.65em;
}
.filter-menu {
position: relative;
}
`;
}
}

View File

@@ -272,6 +272,7 @@ class HassTabsSubpage extends LitElement {
ha-menu-button,
ha-icon-button-arrow-prev,
::slotted([slot="toolbar-icon"]) {
display: flex;
flex-shrink: 0;
pointer-events: auto;
color: var(--sidebar-icon-color);

View File

@@ -78,8 +78,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
super.firstUpdated(changedProps);
this._initializeHass();
setTimeout(() => registerServiceWorker(this), 1000);
/* polyfill for paper-dropdown */
import("web-animations-js/web-animations-next-lite.min");
this.addEventListener("hass-suspend-when-hidden", (ev) => {
this._updateHass({ suspendWhenHidden: ev.detail.suspend });
storeState(this.hass!);

View File

@@ -140,8 +140,6 @@ class OnboardingIntegrations extends LitElement {
this._scanUSBDevices();
loadConfigFlowDialog();
this._loadConfigEntries();
/* polyfill for paper-dropdown */
import("web-animations-js/web-animations-next-lite.min");
}
private _createFlow() {

View File

@@ -16,7 +16,7 @@ import "../../../../components/ha-card";
import "../../../../components/ha-alert";
import "../../../../components/ha-icon-button";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import type { Action } from "../../../../data/script";
import type { Action, ServiceSceneAction } from "../../../../data/script";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@@ -44,8 +44,28 @@ const OPTIONS = [
"device_id",
];
const getType = (action: Action | undefined) =>
action ? OPTIONS.find((option) => option in action) : undefined;
const getType = (action: Action | undefined) => {
if (!action) {
return undefined;
}
if ("metadata" in action && action.service) {
switch (action.service) {
case "scene.turn_on":
// we dont support arrays of entities
if (
!Array.isArray(
(action as unknown as ServiceSceneAction).target?.entity_id
)
) {
return "scene";
}
break;
default:
break;
}
}
return OPTIONS.find((option) => option in action);
};
declare global {
// for fire event

View File

@@ -1,5 +1,4 @@
import { mdiDelete } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";

View File

@@ -16,11 +16,23 @@ export class HaSceneAction extends LitElement implements ActionElement {
@property() public action!: SceneAction;
public static get defaultConfig(): SceneAction {
return { scene: "" };
return {
service: "scene.turn_on",
target: {
entity_id: "",
},
metadata: {},
};
}
protected render() {
const { scene } = this.action;
let scene;
if ("scene" in this.action) {
scene = this.action.scene;
} else {
scene = this.action.target?.entity_id;
}
return html`
<ha-entity-picker
@@ -36,7 +48,13 @@ export class HaSceneAction extends LitElement implements ActionElement {
private _entityPicked(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.action, scene: ev.detail.value },
value: {
service: "scene.turn_on",
target: {
entity_id: ev.detail.value,
},
metadata: {},
},
});
}
}

View File

@@ -1,4 +1,3 @@
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { any, assert, object, optional, string } from "superstruct";

View File

@@ -1,7 +1,6 @@
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";

View File

@@ -1,4 +1,5 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -48,41 +49,35 @@ export class HaTriggerCondition extends LitElement {
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers"
);
}
return html`<paper-dropdown-menu-light
return html`<mwc-select
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.id"
)}
no-animations
.value=${id}
@selected=${this._triggerPicked}
>
<paper-listbox
slot="dropdown-content"
.selected=${id}
attr-for-selected="data-trigger-id"
@selected-item-changed=${this._triggerPicked}
>
${ensureArray(this._triggers).map((trigger) =>
trigger.id
? html`
<paper-item data-trigger-id=${trigger.id}>
${trigger.id}
</paper-item>
`
: ""
)}
</paper-listbox>
</paper-dropdown-menu-light>`;
${ensureArray(this._triggers).map((trigger) =>
trigger.id
? html`
<mwc-list-item .value=${trigger.id}>
${trigger.id}
</mwc-list-item>
`
: ""
)}
</mwc-select>`;
}
private _automationUpdated(config?: AutomationConfig) {
this._triggers = config?.trigger;
}
private _triggerPicked(ev: CustomEvent) {
private _triggerPicked(ev) {
ev.stopPropagation();
if (!ev.detail.value) {
if (!ev.target.value) {
return;
}
const newTrigger = ev.detail.value.dataset.triggerId;
const newTrigger = ev.target.value;
if (this.condition.id === newTrigger) {
return;
}

View File

@@ -85,6 +85,7 @@ export class HaManualAutomationEditor extends LitElement {
)}
.value=${this.config.mode ? MODES.indexOf(this.config.mode) : 0}
@selected=${this._modeChanged}
fixedMenuPosition
>
${MODES.map(
(mode) => html`
@@ -317,6 +318,9 @@ export class HaManualAutomationEditor extends LitElement {
ha-entity-toggle {
margin-right: 8px;
}
mwc-select {
margin-top: 8px;
}
`,
];
}

View File

@@ -1,4 +1,4 @@
import { html, LitElement } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
@@ -150,6 +150,13 @@ export class HaDeviceTrigger extends LitElement {
`ui.panel.config.automation.editor.triggers.type.device.extra_fields.${schema.name}`
) || schema.name;
}
static styles = css`
ha-device-picker {
display: block;
margin-bottom: 8px;
}
`;
}
declare global {

View File

@@ -1,13 +1,12 @@
import { css, html, LitElement } from "lit";
import "../../../../../components/ha-form/ha-form";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-picker";
import type { HaRadio } from "../../../../../components/ha-radio";
import { HaFormSchema } from "../../../../../components/ha-form/types";
import type { GeoLocationTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-trigger-row";
const includeDomains = ["zone"];
import type { LocalizeFunc } from "../../../../../common/translations/localize";
@customElement("ha-automation-trigger-geo_location")
export class HaGeolocationTrigger extends LitElement {
@@ -15,6 +14,30 @@ export class HaGeolocationTrigger extends LitElement {
@property({ attribute: false }) public trigger!: GeoLocationTrigger;
private _schema = memoizeOne((localize: LocalizeFunc) => [
{ name: "source", selector: { text: {} } },
{ name: "zone", selector: { entity: { domain: "zone" } } },
{
name: "event",
type: "select",
required: true,
options: [
[
"enter",
localize(
"ui.panel.config.automation.editor.triggers.type.geo_location.enter"
),
],
[
"leave",
localize(
"ui.panel.config.automation.editor.triggers.type.geo_location.leave"
),
],
],
},
]);
public static get defaultConfig() {
return {
source: "",
@@ -24,86 +47,27 @@ export class HaGeolocationTrigger extends LitElement {
}
protected render() {
const { source, zone, event } = this.trigger;
return html`
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.geo_location.source"
)}
name="source"
.value=${source}
@value-changed=${this._valueChanged}
></paper-input>
<ha-entity-picker
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.geo_location.zone"
)}
.value=${zone}
@value-changed=${this._zonePicked}
<ha-form
.schema=${this._schema(this.hass.localize)}
.data=${this.trigger}
.hass=${this.hass}
allow-custom-entity
.includeDomains=${includeDomains}
></ha-entity-picker>
<label>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.geo_location.event"
)}
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.geo_location.enter"
)}
>
<ha-radio
name="event"
value="enter"
.checked=${event === "enter"}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.geo_location.leave"
)}
>
<ha-radio
name="event"
value="leave"
.checked=${event === "leave"}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
</label>
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
private _zonePicked(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.trigger, zone: ev.detail.value },
});
const newTrigger = ev.detail.value;
fireEvent(this, "value-changed", { value: newTrigger });
}
private _radioGroupPicked(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
...this.trigger,
event: (ev.target as HaRadio).value,
},
});
}
static styles = css`
label {
display: flex;
align-items: center;
}
`;
private _computeLabelCallback = (schema: HaFormSchema): string =>
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.geo_location.${schema.name}`
);
}
declare global {

View File

@@ -1,11 +1,12 @@
import "../../../../../components/ha-form/ha-form";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { HaRadio } from "../../../../../components/ha-radio";
import type { HassTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-radio";
import type { HaFormSchema } from "../../../../../components/ha-form/types";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
@customElement("ha-automation-trigger-homeassistant")
export class HaHassTrigger extends LitElement {
@@ -13,6 +14,28 @@ export class HaHassTrigger extends LitElement {
@property({ attribute: false }) public trigger!: HassTrigger;
private _schema = memoizeOne((localize: LocalizeFunc) => [
{
name: "event",
type: "select",
required: true,
options: [
[
"start",
localize(
"ui.panel.config.automation.editor.triggers.type.homeassistant.start"
),
],
[
"shutdown",
localize(
"ui.panel.config.automation.editor.triggers.type.homeassistant.shutdown"
),
],
],
},
]);
public static get defaultConfig() {
return {
event: "start" as HassTrigger["event"],
@@ -20,50 +43,28 @@ export class HaHassTrigger extends LitElement {
}
protected render() {
const { event } = this.trigger;
return html`
<label id="eventlabel">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.homeassistant.event"
)}
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.homeassistant.start"
)}
>
<ha-radio
name="event"
value="start"
.checked=${event === "start"}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.homeassistant.shutdown"
)}
>
<ha-radio
name="event"
value="shutdown"
.checked=${event === "shutdown"}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
</label>
<ha-form
.schema=${this._schema(this.hass.localize)}
.data=${this.trigger}
.hass=${this.hass}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _radioGroupPicked(ev) {
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
...this.trigger,
event: (ev.target as HaRadio).value,
},
});
const newTrigger = ev.detail.value;
fireEvent(this, "value-changed", { value: newTrigger });
}
private _computeLabelCallback = (schema: HaFormSchema): string =>
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.geo_location.${schema.name}`
);
static styles = css`
label {
display: flex;

View File

@@ -1,12 +1,15 @@
import "@polymer/paper-input/paper-input";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { HaFormSchema } from "../../../../../components/ha-form/types";
import { MqttTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import {
handleChangeEvent,
TriggerElement,
} from "../ha-automation-trigger-row";
import type { TriggerElement } from "../ha-automation-trigger-row";
const SCHEMA: HaFormSchema[] = [
{ name: "topic", required: true, selector: { text: {} } },
{ name: "payload", selector: { text: {} } },
];
@customElement("ha-automation-trigger-mqtt")
export class HaMQTTTrigger extends LitElement implements TriggerElement {
@@ -19,30 +22,27 @@ export class HaMQTTTrigger extends LitElement implements TriggerElement {
}
protected render() {
const { topic, payload } = this.trigger;
return html`
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.mqtt.topic"
)}
name="topic"
.value=${topic}
<ha-form
.schema=${SCHEMA}
.data=${this.trigger}
.hass=${this.hass}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.mqtt.payload"
)}
name="payload"
.value=${payload}
@value-changed=${this._valueChanged}
></paper-input>
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
ev.stopPropagation();
const newTrigger = ev.detail.value;
fireEvent(this, "value-changed", { value: newTrigger });
}
private _computeLabelCallback = (schema: HaFormSchema): string =>
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.mqtt.${schema.name}`
);
}
declare global {

View File

@@ -1,15 +1,13 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import "../../../../../components/ha-form/ha-form";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { HaFormSchema } from "../../../../../components/ha-form/types";
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template";
import "../../../../../components/entity/ha-entity-picker";
import { NumericStateTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-trigger-row";
import "../../../../../components/ha-duration-input";
import type { NumericStateTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
@customElement("ha-automation-trigger-numeric_state")
export class HaNumericStateTrigger extends LitElement {
@@ -17,6 +15,22 @@ export class HaNumericStateTrigger extends LitElement {
@property() public trigger!: NumericStateTrigger;
private _schema = memoizeOne((entityId): HaFormSchema[] => [
{ name: "entity_id", selector: { entity: {} } },
{
name: "attribute",
selector: { attribute: { entity_id: entityId } },
},
{ name: "above", required: false, selector: { text: {} } },
{ name: "below", required: false, selector: { text: {} } },
{
name: "value_template",
required: false,
selector: { text: { multiline: true } },
},
{ name: "for", required: false, selector: { duration: {} } },
]);
public willUpdate(changedProperties: PropertyValues) {
if (!changedProperties.has("trigger")) {
return;
@@ -38,67 +52,46 @@ export class HaNumericStateTrigger extends LitElement {
}
public render() {
const { value_template, entity_id, attribute, below, above } = this.trigger;
const trgFor = createDurationData(this.trigger.for);
const data = { ...this.trigger, for: trgFor };
const schema = this._schema(this.trigger.entity_id);
return html`
<ha-entity-picker
.value=${entity_id}
@value-changed=${this._valueChanged}
.name=${"entity_id"}
<ha-form
.hass=${this.hass}
allow-custom-entity
></ha-entity-picker>
<ha-entity-attribute-picker
.hass=${this.hass}
.entityId=${entity_id}
.value=${attribute}
.name=${"attribute"}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.attribute"
)}
.data=${data}
.schema=${schema}
@value-changed=${this._valueChanged}
allow-custom-value
></ha-entity-attribute-picker>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.above"
)}
name="above"
.value=${above}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.below"
)}
name="below"
.value=${below}
@value-changed=${this._valueChanged}
></paper-input>
<paper-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.value_template"
)}
name="value_template"
.value=${value_template}
@value-changed=${this._valueChanged}
dir="ltr"
></paper-textarea>
<ha-duration-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.for"
)}
.name=${"for"}
.data=${trgFor}
@value-changed=${this._valueChanged}
></ha-duration-input>
.computeLabel=${this._computeLabelCallback}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
ev.stopPropagation();
const newTrigger = ev.detail.value;
fireEvent(this, "value-changed", { value: newTrigger });
}
private _computeLabelCallback = (schema: HaFormSchema): string => {
switch (schema.name) {
case "entity_id":
return this.hass.localize("ui.components.entity.entity-picker.entity");
case "attribute":
return this.hass.localize(
"ui.components.entity.entity-attribute-picker.attribute"
);
case "for":
return this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.state.for`
);
default:
return this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.numeric_state.${schema.name}`
);
}
};
}
declare global {

View File

@@ -34,13 +34,10 @@ const stateTriggerStruct = assign(
const SCHEMA = [
{ name: "entity_id", selector: { entity: {} } },
{
name: "attribute",
selector: { attribute: { entity_id: "" } },
},
{ name: "from", selector: { text: {} } },
{ name: "to", selector: { text: {} } },
{ name: "for", selector: { duration: {} } },
{ name: "attribute", selector: { attribute: { entity_id: "" } } },
{ name: "from", required: false, selector: { text: {} } },
{ name: "to", required: false, selector: { text: {} } },
{ name: "for", required: false, selector: { duration: {} } },
];
@customElement("ha-automation-trigger-state")

View File

@@ -1,16 +1,12 @@
import "@polymer/paper-input/paper-input";
import { css, html, LitElement } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-radio";
import "../../../../../components/ha-formfield";
import type { HaRadio } from "../../../../../components/ha-radio";
import type { SunTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import {
handleChangeEvent,
TriggerElement,
} from "../ha-automation-trigger-row";
import type { TriggerElement } from "../ha-automation-trigger-row";
import type { HaFormSchema } from "../../../../../components/ha-form/types";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
@customElement("ha-automation-trigger-sun")
export class HaSunTrigger extends LitElement implements TriggerElement {
@@ -18,6 +14,29 @@ export class HaSunTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public trigger!: SunTrigger;
private _schema = memoizeOne((localize: LocalizeFunc) => [
{
name: "event",
type: "select",
required: true,
options: [
[
"sunrise",
localize(
"ui.panel.config.automation.editor.triggers.type.sun.sunrise"
),
],
[
"sunset",
localize(
"ui.panel.config.automation.editor.triggers.type.sun.sunset"
),
],
],
},
{ name: "offset", selector: { text: {} } },
]);
public static get defaultConfig() {
return {
event: "sunrise" as SunTrigger["event"],
@@ -26,69 +45,27 @@ export class HaSunTrigger extends LitElement implements TriggerElement {
}
protected render() {
const { offset, event } = this.trigger;
return html`
<label>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.sun.event"
)}
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.sun.sunrise"
)}
>
<ha-radio
name="event"
value="sunrise"
.checked=${event === "sunrise"}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.sun.sunset"
)}
>
<ha-radio
name="event"
value="sunset"
.checked=${event === "sunset"}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
</label>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.sun.offset"
)}
name="offset"
.value=${offset}
<ha-form
.schema=${this._schema(this.hass.localize)}
.data=${this.trigger}
.hass=${this.hass}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></paper-input>
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
}
private _radioGroupPicked(ev) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
...this.trigger,
event: (ev.target as HaRadio).value,
},
});
const newTrigger = ev.detail.value;
fireEvent(this, "value-changed", { value: newTrigger });
}
static styles = css`
label {
display: flex;
align-items: center;
}
`;
private _computeLabelCallback = (schema: HaFormSchema): string =>
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.sun.${schema.name}`
);
}
declare global {

View File

@@ -1,13 +1,13 @@
import "@polymer/paper-input/paper-input";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../../../../common/string/compare";
import { TagTrigger } from "../../../../../data/automation";
import { fetchTags, Tag } from "../../../../../data/tag";
import { HomeAssistant } from "../../../../../types";
import { TriggerElement } from "../ha-automation-trigger-row";
import "../../../../../components/ha-paper-dropdown-menu";
import { caseInsensitiveStringCompare } from "../../../../../common/string/compare";
@customElement("ha-automation-trigger-tag")
export class HaTagTrigger extends LitElement implements TriggerElement {
@@ -29,27 +29,22 @@ export class HaTagTrigger extends LitElement implements TriggerElement {
protected render() {
const { tag_id } = this.trigger;
return html`
<ha-paper-dropdown-menu
<mwc-select
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.tag.label"
)}
?disabled=${this._tags.length === 0}
.disabled=${this._tags.length === 0}
.value=${tag_id}
@selected=${this._tagChanged}
>
<paper-listbox
slot="dropdown-content"
.selected=${tag_id}
attr-for-selected="tag_id"
@iron-select=${this._tagChanged}
>
${this._tags.map(
(tag) => html`
<paper-item tag_id=${tag.id} .tag=${tag}>
${tag.name || tag.id}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
${this._tags.map(
(tag) => html`
<mwc-list-item .value=${tag.id}>
${tag.name || tag.id}
</mwc-list-item>
`
)}
</mwc-select>
`;
}
@@ -64,8 +59,14 @@ export class HaTagTrigger extends LitElement implements TriggerElement {
fireEvent(this, "value-changed", {
value: {
...this.trigger,
tag_id: ev.detail.item.tag.id,
tag_id: ev.target.value,
},
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-tag": HaTagTrigger;
}
}

View File

@@ -1,12 +1,16 @@
import "@polymer/paper-input/paper-input";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { TimePatternTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import {
handleChangeEvent,
TriggerElement,
} from "../ha-automation-trigger-row";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { HaFormSchema } from "../../../../../components/ha-form/types";
import type { TimePatternTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import type { TriggerElement } from "../ha-automation-trigger-row";
const SCHEMA: HaFormSchema[] = [
{ name: "hours", selector: { text: {} } },
{ name: "minutes", selector: { text: {} } },
{ name: "seconds", selector: { text: {} } },
];
@customElement("ha-automation-trigger-time_pattern")
export class HaTimePatternTrigger extends LitElement implements TriggerElement {
@@ -19,38 +23,27 @@ export class HaTimePatternTrigger extends LitElement implements TriggerElement {
}
protected render() {
const { hours, minutes, seconds } = this.trigger;
return html`
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.time_pattern.hours"
)}
name="hours"
.value=${hours}
<ha-form
.hass=${this.hass}
.schema=${SCHEMA}
.data=${this.trigger}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.time_pattern.minutes"
)}
name="minutes"
.value=${minutes}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.time_pattern.seconds"
)}
name="seconds"
.value=${seconds}
@value-changed=${this._valueChanged}
></paper-input>
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev);
ev.stopPropagation();
const newTrigger = ev.detail.value;
fireEvent(this, "value-changed", { value: newTrigger });
}
private _computeLabelCallback = (schema: HaFormSchema): string =>
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.time_pattern.${schema.name}`
);
}
declare global {

View File

@@ -1,5 +1,4 @@
import "@material/mwc-button";
import "@polymer/paper-item/paper-item-body";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";

View File

@@ -5,18 +5,17 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../../../common/string/compare";
import "../../../../components/ha-card";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-switch";
import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud";
import {
CloudStatusLoggedIn,
CloudTTSInfo,
getCloudTTSInfo,
updateCloudPref,
} from "../../../../data/cloud";
getCloudTtsLanguages,
getCloudTtsSupportedGenders,
} from "../../../../data/cloud/tts";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import { translationMetadata } from "../../../../resources/translations-metadata";
import type { HomeAssistant } from "../../../../types";
import { showTryTtsDialog } from "./show-dialog-cloud-tts-try";
@@ -37,7 +36,11 @@ export class CloudTTSPref extends LitElement {
const languages = this.getLanguages(this.ttsInfo);
const defaultVoice = this.cloudStatus.prefs.tts_default_voice;
const genders = this.getSupportedGenders(defaultVoice[0], this.ttsInfo);
const genders = this.getSupportedGenders(
defaultVoice[0],
this.ttsInfo,
this.hass.localize
);
return html`
<ha-card
@@ -100,61 +103,9 @@ export class CloudTTSPref extends LitElement {
}
}
private getLanguages = memoizeOne((info?: CloudTTSInfo) => {
const languages: Array<[string, string]> = [];
private getLanguages = memoizeOne(getCloudTtsLanguages);
if (!info) {
return languages;
}
const seen = new Set<string>();
for (const [lang] of info.languages) {
if (seen.has(lang)) {
continue;
}
seen.add(lang);
let label = lang;
if (lang in translationMetadata.translations) {
label = translationMetadata.translations[lang].nativeName;
} else {
const [langFamily, dialect] = lang.split("-");
if (langFamily in translationMetadata.translations) {
label = `${translationMetadata.translations[langFamily].nativeName}`;
if (langFamily.toLowerCase() !== dialect.toLowerCase()) {
label += ` (${dialect})`;
}
}
}
languages.push([lang, label]);
}
return languages.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1]));
});
private getSupportedGenders = memoizeOne(
(language: string, info?: CloudTTSInfo) => {
const genders: Array<[string, string]> = [];
if (!info) {
return genders;
}
for (const [curLang, gender] of info.languages) {
if (curLang === language) {
genders.push([
gender,
this.hass.localize(`ui.panel.config.cloud.account.tts.${gender}`) ||
gender,
]);
}
}
return genders.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1]));
}
);
private getSupportedGenders = memoizeOne(getCloudTtsSupportedGenders);
private _openTryDialog() {
showTryTtsDialog(this, {
@@ -170,7 +121,11 @@ export class CloudTTSPref extends LitElement {
const language = ev.target.value;
const curGender = this.cloudStatus!.prefs.tts_default_voice[1];
const genders = this.getSupportedGenders(language, this.ttsInfo);
const genders = this.getSupportedGenders(
language,
this.ttsInfo,
this.hass.localize
);
const newGender = genders.find((item) => item[0] === curGender)
? curGender
: genders[0][0];

View File

@@ -1,5 +1,3 @@
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";

View File

@@ -1,5 +1,4 @@
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { css, html, LitElement, TemplateResult } from "lit";

View File

@@ -266,9 +266,6 @@ export class CloudRegister extends LitElement {
a {
color: var(--primary-color);
}
paper-item {
cursor: pointer;
}
h1 {
margin: 0;
}

View File

@@ -1,5 +1,4 @@
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";

View File

@@ -165,7 +165,7 @@ export class HaDeviceEntitiesCard extends LitElement {
const stateObj = this.hass.states[entry.entity_id];
const name = stripPrefixFromEntityName(
computeStateName(stateObj),
`${this.deviceName} `.toLowerCase()
this.deviceName.toLowerCase()
);
if (name) {
config.name = name;
@@ -198,7 +198,7 @@ export class HaDeviceEntitiesCard extends LitElement {
${name
? stripPrefixFromEntityName(
name,
`${this.deviceName} `.toLowerCase()
this.deviceName.toLowerCase()
) || name
: entry.entity_id}
</div>

View File

@@ -71,6 +71,7 @@ class DialogMQTTDeviceDebugInfo extends LitElement {
<ha-switch
.checked=${this._showDeserialized}
@change=${this._showDeserializedChanged}
dialogInitialFocus
>
</ha-switch>
</ha-formfield>
@@ -164,6 +165,7 @@ class DialogMQTTDeviceDebugInfo extends LitElement {
<code>${topic.topic}</code>
<mqtt-messages
.hass=${this.hass}
direction="Received"
.messages=${topic.messages}
.showDeserialized=${this._showDeserialized}
.showAsYaml=${this._showAsYaml}
@@ -174,7 +176,31 @@ class DialogMQTTDeviceDebugInfo extends LitElement {
topic.messages.length
)}
>
</mqtt-messages>
</mqtt-rx-messages>
</li>
`
)}
</ul>
Transmitted messages:
<ul>
${entity.transmitted.map(
(topic) => html`
<li>
<code>${topic.topic}</code>
<mqtt-messages
.hass=${this.hass}
direction="Transmitted"
.messages=${topic.messages}
.showDeserialized=${this._showDeserialized}
.showAsYaml=${this._showAsYaml}
.subscribedTopic=${topic.topic}
.summary=${this.hass!.localize(
"ui.dialogs.mqtt_device_debug_info.recent_tx_messages",
"n",
topic.messages.length
)}
>
</mqtt-tx-messages>
</li>
`
)}

View File

@@ -12,6 +12,8 @@ class MQTTMessages extends LitElement {
@property() public messages!: MQTTMessage[];
@property() public direction!: string;
@property() public showAsYaml = false;
@property() public showDeserialized = false;
@@ -50,7 +52,7 @@ class MQTTMessages extends LitElement {
(message) => html`
<li class="message">
<div class="time">
Received
${this.direction}
${formatTimeWithSeconds(
new Date(message.time),
this.hass.locale

View File

@@ -10,8 +10,10 @@ import {
import { customElement, property, state } from "lit/decorators";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import {
fetchZwaveNodeStatus,
getZwaveJsIdentifiersFromDevice,
ZWaveJSNodeIdentifiers,
ZWaveJSNodeStatus,
} from "../../../../../../data/zwave_js";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
@@ -29,43 +31,67 @@ export class HaDeviceActionsZWaveJS extends LitElement {
@state() private _nodeId?: number;
@state() private _node?: ZWaveJSNodeStatus;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) {
this._entryId = this.device.config_entries[0];
const identifiers: ZWaveJSNodeIdentifiers | undefined =
getZwaveJsIdentifiersFromDevice(this.device);
if (!identifiers) {
return;
}
this._nodeId = identifiers.node_id;
this._entryId = this.device.config_entries[0];
this._fetchNodeDetails();
}
}
protected async _fetchNodeDetails() {
if (!this._nodeId || !this._entryId) {
return;
}
this._node = await fetchZwaveNodeStatus(
this.hass,
this._entryId,
this._nodeId
);
}
protected render(): TemplateResult {
if (!this._node) {
return html``;
}
return html`
<a
.href=${`/config/zwave_js/node_config/${this.device.id}?config_entry=${this._entryId}`}
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.device_config"
)}
</mwc-button>
</a>
<mwc-button @click=${this._reinterviewClicked}>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.reinterview_device"
)}
</mwc-button>
<mwc-button @click=${this._healNodeClicked}>
${this.hass.localize("ui.panel.config.zwave_js.device_info.heal_node")}
</mwc-button>
<mwc-button @click=${this._removeFailedNode}>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.remove_failed"
)}
</mwc-button>
${!this._node.is_controller_node
? html`
<a
.href=${`/config/zwave_js/node_config/${this.device.id}?config_entry=${this._entryId}`}
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.device_config"
)}
</mwc-button>
</a>
<mwc-button @click=${this._reinterviewClicked}>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.reinterview_device"
)}
</mwc-button>
<mwc-button @click=${this._healNodeClicked}>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.heal_node"
)}
</mwc-button>
<mwc-button @click=${this._removeFailedNode}>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.remove_failed"
)}
</mwc-button>
`
: ""}
`;
}

View File

@@ -103,52 +103,58 @@ export class HaDeviceInfoZWaveJS extends LitElement {
${this.hass.localize("ui.panel.config.zwave_js.common.node_id")}:
${this._node.node_id}
</div>
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.node_status"
)}:
${this.hass.localize(
`ui.panel.config.zwave_js.node_status.${
nodeStatus[this._node.status]
}`
)}
</div>
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.node_ready"
)}:
${this._node.ready
? this.hass.localize("ui.common.yes")
: this.hass.localize("ui.common.no")}
</div>
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.highest_security"
)}:
${this._node.highest_security_class !== null
? this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${
SecurityClass[this._node.highest_security_class]
}.title`
)
: this._node.is_secure === false
? this.hass.localize(
"ui.panel.config.zwave_js.security_classes.none.title"
)
: this.hass.localize("ui.panel.config.zwave_js.device_info.unknown")}
</div>
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.zwave_plus"
)}:
${this._node.zwave_plus_version
? this.hass.localize(
"ui.panel.config.zwave_js.device_info.zwave_plus_version",
"version",
this._node.zwave_plus_version
)
: this.hass.localize("ui.common.no")}
</div>
${!this._node.is_controller_node
? html`
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.node_status"
)}:
${this.hass.localize(
`ui.panel.config.zwave_js.node_status.${
nodeStatus[this._node.status]
}`
)}
</div>
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.node_ready"
)}:
${this._node.ready
? this.hass.localize("ui.common.yes")
: this.hass.localize("ui.common.no")}
</div>
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.highest_security"
)}:
${this._node.highest_security_class !== null
? this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${
SecurityClass[this._node.highest_security_class]
}.title`
)
: this._node.is_secure === false
? this.hass.localize(
"ui.panel.config.zwave_js.security_classes.none.title"
)
: this.hass.localize(
"ui.panel.config.zwave_js.device_info.unknown"
)}
</div>
<div>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.zwave_plus"
)}:
${this._node.zwave_plus_version
? this.hass.localize(
"ui.panel.config.zwave_js.device_info.zwave_plus_version",
"version",
this._node.zwave_plus_version
)
: this.hass.localize("ui.common.no")}
</div>
`
: ""}
`;
}

View File

@@ -1,6 +1,7 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import "@polymer/paper-input/paper-input";
import type { PaperItemElement } from "@polymer/paper-item/paper-item";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
@@ -17,7 +18,6 @@ import { domainIcon } from "../../../common/entity/domain_icon";
import "../../../components/ha-area-picker";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-icon-picker";
import "../../../components/ha-paper-dropdown-menu";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import {
@@ -158,28 +158,23 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
></ha-icon-picker>
${OVERRIDE_DEVICE_CLASSES[domain]?.includes(this._deviceClass) ||
(domain === "cover" && this.entry.original_device_class === null)
? html`<ha-paper-dropdown-menu
? html`<mwc-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_class"
)}
.value=${this._deviceClass}
@selected=${this._deviceClassChanged}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-value"
.selected=${this._deviceClass}
@selected-item-changed=${this._deviceClassChanged}
>
${OVERRIDE_DEVICE_CLASSES[domain].map(
(deviceClass: string) => html`
<paper-item .itemValue=${deviceClass}>
${this.hass.localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
)}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>`
${OVERRIDE_DEVICE_CLASSES[domain].map(
(deviceClass: string) => html`
<mwc-list-item .value=${deviceClass}>
${this.hass.localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
)}
</mwc-list-item>
`
)}
</mwc-select>`
: ""}
<paper-input
.value=${this._entityId}
@@ -302,12 +297,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._entityId = ev.detail.value;
}
private _deviceClassChanged(ev: PolymerChangedEvent<PaperItemElement>): void {
private _deviceClassChanged(ev): void {
this._error = undefined;
if (ev.detail.value === null) {
return;
}
this._deviceClass = (ev.detail.value as any).itemValue;
this._deviceClass = ev.target.value;
}
private _areaPicked(ev: CustomEvent) {
@@ -425,7 +417,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
padding-bottom: max(env(safe-area-inset-bottom), 8px);
background-color: var(--mdc-theme-surface, #fff);
}
ha-paper-dropdown-menu {
mwc-select {
width: 100%;
}
ha-switch {

View File

@@ -22,10 +22,6 @@ documentContainer.innerHTML = `<dom-module id="ha-form-style">
@apply --layout-vertical;
@apply --layout-start;
}
paper-dropdown-menu.form-control {
margin: -9px 0;
}
</style>
</template>
</dom-module>`;

View File

@@ -21,8 +21,6 @@ import {
mdiTools,
mdiViewDashboard,
} from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { PolymerElement } from "@polymer/polymer";
import { PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";

View File

@@ -2,7 +2,7 @@ import "@material/mwc-button/mwc-button";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-tooltip/paper-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";

View File

@@ -297,29 +297,36 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
this._filter
);
const filterMenu = html`<ha-button-menu
corner="BOTTOM_START"
multi
slot=${ifDefined(this.narrow ? "toolbar-icon" : undefined)}
@action=${this._handleMenuAction}
const filterMenu = html`<div
slot=${ifDefined(this.narrow ? "toolbar-icon" : "suffix")}
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiFilterVariant}
${!this._showDisabled && this.narrow && disabledCount
? html`<span class="badge">${disabledCount}</span>`
: ""}
<ha-button-menu
corner="BOTTOM_START"
multi
@action=${this._handleMenuAction}
@click=${this._preventDefault}
>
</ha-icon-button>
<ha-check-list-item left .selected=${this._showIgnored}>
${this.hass.localize(
"ui.panel.config.integrations.ignore.show_ignored"
)}
</ha-check-list-item>
<ha-check-list-item left .selected=${this._showDisabled}>
${this.hass.localize(
"ui.panel.config.integrations.disable.show_disabled"
)}
</ha-check-list-item>
</ha-button-menu>`;
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiFilterVariant}
>
</ha-icon-button>
<ha-check-list-item left .selected=${this._showIgnored}>
${this.hass.localize(
"ui.panel.config.integrations.ignore.show_ignored"
)}
</ha-check-list-item>
<ha-check-list-item left .selected=${this._showDisabled}>
${this.hass.localize(
"ui.panel.config.integrations.disable.show_disabled"
)}
</ha-check-list-item>
</ha-button-menu>
</div>`;
return html`
<hass-tabs-subpage
@@ -336,8 +343,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.filter=${this._filter}
class="header"
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
.label=${this.hass.localize(
"ui.panel.config.integrations.search"
@@ -350,29 +355,33 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
<div class="search">
<search-input
.hass=${this.hass}
no-label-float
no-underline
suffix
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
.label=${this.hass.localize(
"ui.panel.config.integrations.search"
)}
></search-input>
${!this._showDisabled && disabledCount
? html`<div class="active-filters">
${this.hass.localize(
"ui.panel.config.integrations.disable.disabled_integrations",
{ number: disabledCount }
)}
<mwc-button
@click=${this._toggleShowDisabled}
.label=${this.hass.localize(
"ui.panel.config.integrations.disable.show"
>
${!this._showDisabled && disabledCount
? html`<div
class="active-filters"
slot="suffix"
@click=${this._preventDefault}
>
${this.hass.localize(
"ui.panel.config.integrations.disable.disabled_integrations",
{ number: disabledCount }
)}
></mwc-button>
</div>`
: ""}
${filterMenu}
<mwc-button
@click=${this._toggleShowDisabled}
.label=${this.hass.localize(
"ui.panel.config.integrations.disable.show"
)}
></mwc-button>
</div>`
: ""}
${filterMenu}
</search-input>
</div>
`}
@@ -503,6 +512,10 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
`;
}
private _preventDefault(ev) {
ev.preventDefault();
}
private _loadConfigEntries() {
getConfigEntries(this.hass).then((configEntries) => {
this._configEntries = configEntries
@@ -683,13 +696,15 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
.empty-message h1 {
margin-bottom: 0;
}
search-input {
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
--text-field-overflow: visible;
}
search-input.header {
display: block;
color: var(--secondary-text-color);
margin-left: 8px;
--mdc-text-field-fill-color: transparant;
--mdc-text-field-idle-line-color: var(--divider-color);
--mdc-ripple-color: transparant;
}
.search {
@@ -717,6 +732,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
align-items: center;
padding: 2px 2px 2px 8px;
font-size: 14px;
width: max-content;
cursor: initial;
}
.active-filters mwc-button {
margin-left: 8px;
@@ -732,6 +749,24 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
left: 0;
content: "";
}
.badge {
min-width: 20px;
box-sizing: border-box;
border-radius: 50%;
font-weight: 400;
background-color: var(--primary-color);
line-height: 20px;
text-align: center;
padding: 0px 4px;
color: var(--text-primary-color);
position: absolute;
right: 14px;
top: 8px;
font-size: 0.65em;
}
ha-button-menu {
color: var(--primary-text-color);
}
`,
];
}

View File

@@ -1,11 +1,8 @@
import { Select } from "@material/mwc-select";
import { Cluster, ZHADevice } from "../../../../../data/zha";
export interface PickerTarget extends EventTarget {
selected: number;
}
export interface ItemSelectedEvent {
target?: PickerTarget;
target?: Select;
}
export interface ZHADeviceRemovedEvent {

View File

@@ -1,9 +1,8 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select";
import { mdiHelpCircle } from "@mdi/js";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResultGroup,
@@ -13,6 +12,7 @@ import {
TemplateResult,
} from "lit";
import { property, state } from "lit/decorators";
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
@@ -48,7 +48,7 @@ export class ZHAClusterAttributes extends LitElement {
@state() private _attributes: Attribute[] = [];
@state() private _selectedAttributeIndex = -1;
@state() private _selectedAttributeId?: number;
@state() private _attributeValue?: any = "";
@@ -60,7 +60,7 @@ export class ZHAClusterAttributes extends LitElement {
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("selectedCluster")) {
this._attributes = [];
this._selectedAttributeIndex = -1;
this._selectedAttributeId = undefined;
this._attributeValue = "";
this._fetchAttributesForCluster();
}
@@ -92,29 +92,25 @@ export class ZHAClusterAttributes extends LitElement {
<ha-card class="content">
<div class="attribute-picker">
<paper-dropdown-menu
label=${this.hass!.localize(
<mwc-select
.label=${this.hass!.localize(
"ui.panel.config.zha.cluster_attributes.attributes_of_cluster"
)}
class="menu"
.value=${String(this._selectedAttributeId)}
@selected=${this._selectedAttributeChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
<paper-listbox
slot="dropdown-content"
.selected=${this._selectedAttributeIndex}
@iron-select=${this._selectedAttributeChanged}
>
${this._attributes.map(
(entry) => html`
<paper-item
>${entry.name +
" (id: " +
formatAsPaddedHex(entry.id) +
")"}</paper-item
>
`
)}
</paper-listbox>
</paper-dropdown-menu>
${this._attributes.map(
(entry) => html`
<mwc-list-item .value=${String(entry.id)}>
${entry.name + " (id: " + formatAsPaddedHex(entry.id) + ")"}
</mwc-list-item>
`
)}
</mwc-select>
</div>
${this.showHelp
? html`
@@ -125,7 +121,7 @@ export class ZHAClusterAttributes extends LitElement {
</div>
`
: ""}
${this._selectedAttributeIndex !== -1
${this._selectedAttributeId !== undefined
? this._renderAttributeInteractions()
: ""}
</ha-card>
@@ -218,7 +214,7 @@ export class ZHAClusterAttributes extends LitElement {
endpoint_id: this.selectedCluster!.endpoint_id,
cluster_id: this.selectedCluster!.id,
cluster_type: this.selectedCluster!.type,
attribute: this._attributes[this._selectedAttributeIndex].id,
attribute: this._selectedAttributeId!,
manufacturer: this._manufacturerCodeOverride
? parseInt(this._manufacturerCodeOverride as string, 10)
: undefined,
@@ -236,7 +232,7 @@ export class ZHAClusterAttributes extends LitElement {
endpoint_id: this.selectedCluster!.endpoint_id,
cluster_id: this.selectedCluster!.id,
cluster_type: this.selectedCluster!.type,
attribute: this._attributes[this._selectedAttributeIndex].id,
attribute: this._selectedAttributeId!,
value: this._attributeValue,
manufacturer: this._manufacturerCodeOverride
? parseInt(this._manufacturerCodeOverride as string, 10)
@@ -266,7 +262,7 @@ export class ZHAClusterAttributes extends LitElement {
}
private _selectedAttributeChanged(event: ItemSelectedEvent): void {
this._selectedAttributeIndex = event.target!.selected;
this._selectedAttributeId = Number(event.target!.value);
this._attributeValue = "";
}
@@ -274,6 +270,10 @@ export class ZHAClusterAttributes extends LitElement {
return [
haStyle,
css`
mwc-select {
margin-top: 16px;
}
.menu {
width: 100%;
}

View File

@@ -1,8 +1,7 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select";
import { mdiHelpCircle } from "@mdi/js";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResultGroup,
@@ -12,6 +11,7 @@ import {
TemplateResult,
} from "lit";
import { property, state } from "lit/decorators";
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
@@ -26,11 +26,7 @@ import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
import { formatAsPaddedHex } from "./functions";
import {
ChangeEvent,
IssueCommandServiceData,
ItemSelectedEvent,
} from "./types";
import { ChangeEvent, IssueCommandServiceData } from "./types";
export class ZHAClusterCommands extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@@ -45,7 +41,7 @@ export class ZHAClusterCommands extends LitElement {
@state() private _commands: Command[] = [];
@state() private _selectedCommandIndex = -1;
@state() private _selectedCommandId?: number;
@state() private _manufacturerCodeOverride?: number;
@@ -55,7 +51,7 @@ export class ZHAClusterCommands extends LitElement {
protected updated(changedProperties: PropertyValues): void {
if (changedProperties.has("selectedCluster")) {
this._commands = [];
this._selectedCommandIndex = -1;
this._selectedCommandId = undefined;
this._fetchCommandsForCluster();
}
super.update(changedProperties);
@@ -86,29 +82,25 @@ export class ZHAClusterCommands extends LitElement {
<ha-card class="content">
<div class="command-picker">
<paper-dropdown-menu
label=${this.hass!.localize(
<mwc-select
.label=${this.hass!.localize(
"ui.panel.config.zha.cluster_commands.commands_of_cluster"
)}
class="menu"
.value=${String(this._selectedCommandId)}
@selected=${this._selectedCommandChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
<paper-listbox
slot="dropdown-content"
.selected=${this._selectedCommandIndex}
@iron-select=${this._selectedCommandChanged}
>
${this._commands.map(
(entry) => html`
<paper-item
>${entry.name +
" (id: " +
formatAsPaddedHex(entry.id) +
")"}</paper-item
>
`
)}
</paper-listbox>
</paper-dropdown-menu>
${this._commands.map(
(entry) => html`
<mwc-list-item .value=${String(entry.id)}>
${entry.name + " (id: " + formatAsPaddedHex(entry.id) + ")"}
</mwc-list-item>
`
)}
</mwc-select>
</div>
${this._showHelp
? html`
@@ -119,7 +111,7 @@ export class ZHAClusterCommands extends LitElement {
</div>
`
: ""}
${this._selectedCommandIndex !== -1
${this._selectedCommandId !== undefined
? html`
<div class="input-text">
<paper-input
@@ -187,8 +179,10 @@ export class ZHAClusterCommands extends LitElement {
endpoint_id: this.selectedCluster!.endpoint_id,
cluster_id: this.selectedCluster!.id,
cluster_type: this.selectedCluster!.type,
command: this._commands[this._selectedCommandIndex].id,
command_type: this._commands[this._selectedCommandIndex].type,
command: this._selectedCommandId!,
command_type: this._commands.find(
(command) => command.id === this._selectedCommandId
)!.type,
};
}
@@ -202,8 +196,8 @@ export class ZHAClusterCommands extends LitElement {
this._showHelp = !this._showHelp;
}
private _selectedCommandChanged(event: ItemSelectedEvent): void {
this._selectedCommandIndex = event.target!.selected;
private _selectedCommandChanged(event): void {
this._selectedCommandId = Number(event.target.value);
this._issueClusterCommandServiceData =
this._computeIssueClusterCommandServiceData();
}
@@ -212,6 +206,9 @@ export class ZHAClusterCommands extends LitElement {
return [
haStyle,
css`
mwc-select {
margin-top: 16px;
}
.menu {
width: 100%;
}

View File

@@ -1,7 +1,6 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select";
import { mdiHelpCircle } from "@mdi/js";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResultGroup,
@@ -12,6 +11,7 @@ import {
} from "lit";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
@@ -25,7 +25,6 @@ import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
import { computeClusterKey } from "./functions";
import { ItemSelectedEvent } from "./types";
declare global {
// for fire event
@@ -79,24 +78,25 @@ export class ZHAClusters extends LitElement {
<ha-card class="content">
<div class="node-picker">
<paper-dropdown-menu
<mwc-select
.label=${this.hass!.localize(
"ui.panel.config.zha.common.clusters"
)}
class="menu"
.value=${String(this._selectedClusterIndex)}
@selected=${this._selectedClusterChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
<paper-listbox
slot="dropdown-content"
.selected=${this._selectedClusterIndex}
@iron-select=${this._selectedClusterChanged}
>
${this._clusters.map(
(entry) => html`
<paper-item>${computeClusterKey(entry)}</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
${this._clusters.map(
(entry, idx) => html`
<mwc-list-item .value=${String(idx)}
>${computeClusterKey(entry)}</mwc-list-item
>
`
)}
</mwc-select>
</div>
${this.showHelp
? html`
@@ -122,8 +122,8 @@ export class ZHAClusters extends LitElement {
}
}
private _selectedClusterChanged(event: ItemSelectedEvent): void {
this._selectedClusterIndex = event.target!.selected;
private _selectedClusterChanged(event): void {
this._selectedClusterIndex = Number(event.target!.value);
fireEvent(this, "zha-cluster-selected", {
cluster: this._clusters[this._selectedClusterIndex],
});
@@ -137,6 +137,9 @@ export class ZHAClusters extends LitElement {
return [
haStyle,
css`
mwc-select {
margin-top: 16px;
}
.menu {
width: 100%;
}

View File

@@ -1,7 +1,5 @@
import "@material/mwc-button/mwc-button";
import { mdiFolderMultipleOutline, mdiLan, mdiNetwork, mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import {
css,
CSSResultGroup,

View File

@@ -1,8 +1,7 @@
import "@material/mwc-button/mwc-button";
import { mdiHelpCircle } from "@mdi/js";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select";
import {
css,
CSSResultGroup,
@@ -21,6 +20,7 @@ import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import "../../../ha-config-section";
import { ItemSelectedEvent } from "./types";
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
@customElement("zha-device-binding-control")
export class ZHADeviceBindingControl extends LitElement {
@@ -62,23 +62,25 @@ export class ZHADeviceBindingControl extends LitElement {
<ha-card class="content">
<div class="command-picker">
<paper-dropdown-menu label="Bindable Devices" class="menu">
<paper-listbox
slot="dropdown-content"
.selected=${this._bindTargetIndex}
@iron-select=${this._bindTargetIndexChanged}
>
${this.bindableDevices.map(
(device) => html`
<paper-item
>${device.user_given_name
? device.user_given_name
: device.name}</paper-item
>
`
)}
</paper-listbox>
</paper-dropdown-menu>
<mwc-select
label="Bindable Devices"
class="menu"
.value=${String(this._bindTargetIndex)}
@selected=${this._bindTargetIndexChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${this.bindableDevices.map(
(device, idx) => html`
<mwc-list-item .value=${String(idx)}>
${device.user_given_name
? device.user_given_name
: device.name}
</mwc-list-item>
`
)}
</mwc-select>
</div>
${this._showHelp
? html`
@@ -111,7 +113,7 @@ export class ZHADeviceBindingControl extends LitElement {
}
private _bindTargetIndexChanged(event: ItemSelectedEvent): void {
this._bindTargetIndex = event.target!.selected;
this._bindTargetIndex = Number(event.target!.value);
this._deviceToBind =
this._bindTargetIndex === -1
? undefined

View File

@@ -1,8 +1,5 @@
import "@material/mwc-button/mwc-button";
import { mdiHelpCircle } from "@mdi/js";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResultGroup,
@@ -13,6 +10,7 @@ import {
} from "lit";
import { customElement, property, state, query } from "lit/decorators";
import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
import "../../../../../components/buttons/ha-call-service-button";
import { SelectionChangedEvent } from "../../../../../components/data-table/ha-data-table";
import "../../../../../components/ha-card";
@@ -95,22 +93,24 @@ export class ZHAGroupBindingControl extends LitElement {
<ha-card class="content">
<div class="command-picker">
<paper-dropdown-menu
<mwc-select
.label=${this.hass!.localize(
"ui.panel.config.zha.group_binding.group_picker_label"
)}
class="menu"
.value=${String(this._bindTargetIndex)}
@selected=${this._bindTargetIndexChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
<paper-listbox
slot="dropdown-content"
.selected=${this._bindTargetIndex}
@iron-select=${this._bindTargetIndexChanged}
>
${this.groups.map(
(group) => html` <paper-item>${group.name}</paper-item> `
)}
</paper-listbox>
</paper-dropdown-menu>
${this.groups.map(
(group, idx) =>
html`<mwc-list-item .value=${String(idx)}
>${group.name}</mwc-list-item
> `
)}
</mwc-select>
</div>
${this._showHelp
? html`
@@ -179,7 +179,7 @@ export class ZHAGroupBindingControl extends LitElement {
}
private _bindTargetIndexChanged(event: ItemSelectedEvent): void {
this._bindTargetIndex = event.target!.selected;
this._bindTargetIndex = Number(event.target!.value);
this._groupToBind =
this._bindTargetIndex === -1
? undefined

View File

@@ -440,19 +440,15 @@ export class ZHANetworkVisualizationPage extends LitElement {
search-input {
flex: 1;
display: block;
}
search-input.header {
display: block;
position: relative;
top: -2px;
color: var(--secondary-text-color);
}
ha-device-picker {
flex: 1;
position: relative;
top: -4px;
}
.controls {

View File

@@ -4,6 +4,7 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
@@ -513,11 +514,20 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
ready() {
super.ready();
import("web-animations-js/web-animations-next-lite.min");
this.addEventListener("hass-service-called", (ev) =>
this.serviceCalled(ev)
);
}
attached() {
setCancelSyntheticClickEvents(true);
}
detached() {
setCancelSyntheticClickEvents(false);
}
serviceCalled(ev) {
if (ev.detail.success && ev.detail.service === "set_poll_intensity") {
this._saveEntity();

View File

@@ -1,6 +1,6 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import { mdiDownload } from "@mdi/js";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-listbox/paper-listbox";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultArray, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@@ -77,26 +77,20 @@ class ZWaveJSLogs extends SubscribeMixin(LitElement) {
<div class="card-content">
${this._logConfig
? html`
<paper-dropdown-menu
dynamic-align
<mwc-select
.label=${this.hass.localize(
"ui.panel.config.zwave_js.logs.log_level"
)}
.value=${this._logConfig.level}
@selected=${this._dropdownSelected}
>
<paper-listbox
slot="dropdown-content"
.selected=${this._logConfig.level}
attr-for-selected="value"
@iron-select=${this._dropdownSelected}
>
<paper-item value="error">Error</paper-item>
<paper-item value="warn">Warn</paper-item>
<paper-item value="info">Info</paper-item>
<paper-item value="verbose">Verbose</paper-item>
<paper-item value="debug">Debug</paper-item>
<paper-item value="silly">Silly</paper-item>
</paper-listbox>
</paper-dropdown-menu>
<mwc-list-item value="error">Error</mwc-list-item>
<mwc-list-item value="warn">Warn</mwc-list-item>
<mwc-list-item value="info">Info</mwc-list-item>
<mwc-list-item value="verbose">Verbose</mwc-list-item>
<mwc-list-item value="debug">Debug</mwc-list-item>
<mwc-list-item value="silly">Silly</mwc-list-item>
</mwc-select>
`
: ""}
</div>
@@ -142,7 +136,7 @@ class ZWaveJSLogs extends SubscribeMixin(LitElement) {
if (ev.target === undefined || this._logConfig === undefined) {
return;
}
const selected = ev.target.selected;
const selected = ev.target.value;
if (this._logConfig.level === selected) {
return;
}

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