Compare commits

...

53 Commits

Author SHA1 Message Date
Ludeeus
3703ffc42d Remove data/supervisor/root 2022-03-04 09:47:21 +00:00
Ludeeus
9ea8e13c87 Use the update integration to provide updates 2022-03-04 09:45:20 +00:00
Robin Wittebol
604b79696e Always show tab labels (#11919) 2022-03-03 19:46:14 +01:00
Robin Wittebol
8c445f6409 Fix datepicker triangle (#11920) 2022-03-03 19:45:03 +01:00
Bram Kragten
797c871137 Convert objects to string in config flow error (#11908) 2022-03-03 13:55:40 +01:00
Steve Repsher
24829bd903 Supervisor mobile click accessibility (#11915) 2022-03-03 10:15:22 +01:00
Bram Kragten
add92a559d Fix quickbar overlaying, fix click handling (#11900) 2022-03-02 17:50:01 +01:00
Paulus Schoutsen
17018c0f26 Bumped version to 20220301.0 2022-03-01 14:48:41 -08:00
Joakim Sørensen
cd6a478130 Better handle brands URL in media thumbnails (#11890) 2022-03-01 14:32:56 -08:00
Zack Barett
4f6d7ca5c9 Fix for Entity SElector when supplying multiple domains (#11887)
Fix for Entity SElector when supplying multiple domains
2022-02-28 16:45:33 -08:00
Joakim Sørensen
c2994343b4 Remove unused attributes from search-input (#11889) 2022-02-28 18:27:07 -06:00
Joakim Sørensen
e5f77c35d4 Remove autofocus form log panel search (#11888) 2022-03-01 00:31:09 +01:00
Steve Repsher
a9e5a5dd44 Add a few button labels (#11885)
* Add label to remove addon repository button

* Add label to delete energy device button

* Add label to quick bar button
2022-02-28 23:53:39 +01:00
Bram Kragten
1159798b8d Energy: Wait with subscribe for _config to be set (#11884) 2022-02-28 12:32:36 -06:00
Paulus Schoutsen
437de42c55 Handle resolve media and cannot play errors (#11878) 2022-02-28 12:30:49 +01:00
Bram Kragten
89e0bb3f16 Fix date input in Safari (#11871)
* Fix date input in Safari

* helper
2022-02-28 12:29:53 +01:00
Robin Wittebol
28c9631b6c Remove white border on date-range-picker triangle (#11877) 2022-02-28 12:29:07 +01:00
Paulus Schoutsen
a769f84755 Bumped version to 20220226.0 2022-02-26 13:28:52 -08:00
Bram Kragten
7abf9c2473 Fix condition time (#11866)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-02-26 21:18:33 +00:00
Paulus Schoutsen
298296a81f Fix iOS audio (#11863) 2022-02-26 15:08:42 -06:00
Marc Mueller
6907fa5c8e Add py.typed (#11865) 2022-02-26 13:03:12 -08:00
Zack Barett
546461b70f Fix Render Pane on Mobile (#11856) 2022-02-25 11:51:55 -08:00
Joakim Sørensen
4031009c26 Only set tip once (#11853) 2022-02-25 10:03:55 -06:00
Paulus Schoutsen
91e4557625 Guard controls in more info media player (#11851) 2022-02-25 01:31:59 -06:00
Paulus Schoutsen
f0c4b92dbb Small fixes for actions (#11850) 2022-02-24 21:48:54 -08:00
Zack Barett
04ae8c9d14 Bumped version to 20220224.0 (#11847) 2022-02-24 16:22:09 -08:00
Zack Barett
0158610d42 Finishing up the editors (#11846) 2022-02-24 23:35:28 +00:00
Bram Kragten
5ab6121581 Fix quickbar showing on ha-select (#11845) 2022-02-24 14:24:32 -08:00
Bram Kragten
3d9c31aef9 Allow to clear integration filter on mobile (#11844) 2022-02-24 14:21:25 -08:00
Paulus Schoutsen
acfeea5c92 Show browse media even if media player has no controls (#11843) 2022-02-24 14:17:33 -08:00
Zack Barett
75e8e17073 Statistics Graph Editor to Ha Form (#11820) 2022-02-24 22:49:43 +01:00
Zack Barett
976fd4b32d weather forecast editor to HA form (#11823) 2022-02-24 22:49:20 +01:00
Bram Kragten
49beafbe5f Fix dev states filter field on iOS (#11839) 2022-02-24 21:57:20 +01:00
Bram Kragten
151f8d5524 Fix time trigger (#11841) 2022-02-24 14:36:18 -06:00
Bram Kragten
48355aa98e Fix icon color in select (#11842) 2022-02-24 14:35:09 -06:00
Paulus Schoutsen
fc31929f41 Fix saving home name (#11838) 2022-02-24 18:51:56 +00:00
Paulus Schoutsen
b7c149fcc1 Fix timer entity exception (#11837) 2022-02-24 10:30:45 -08:00
Paulus Schoutsen
02d058561b Lovelace: Datetime and text to match look select (#11836) 2022-02-24 18:16:42 +00:00
Zack Barett
4e57fb1ec1 Fix for Sidebar view rendering issue (#11835) 2022-02-24 10:15:34 -08:00
Patrick ZAJDA
30f79c5a46 Ask confirmation before logging out from Home Assistant Cloud (#11833)
Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-02-24 10:13:19 -08:00
Zack Barett
30f7252d84 Update Media Browser Bar to show image on mobile but a bit less (#11818) 2022-02-24 11:56:20 -06:00
Bram Kragten
8af795a7ce Make automation name field wide (#11832) 2022-02-24 09:52:40 -06:00
Joakim Sørensen
8576eeae41 Add tip rotation on dashboard (#11826)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-24 09:07:32 -06:00
Paulus Schoutsen
cd740ed135 Fix cast receiver background (#11825)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-24 10:52:12 +00:00
Zack Barett
892f774792 Picture Glance to HA Form (#11821) 2022-02-23 21:40:04 -08:00
Zack Barett
aa504fe1f8 Shopping list to MWC (#11822) 2022-02-23 21:36:58 -08:00
Paulus Schoutsen
be491451d5 Add run action to dropdown (#11817) 2022-02-23 21:36:25 -08:00
Zack Barett
bad184210d Control where the tip breaks (#11819) 2022-02-23 19:42:32 -06:00
Bram Kragten
a43b3b64b3 Link to filtered logs (#11816) 2022-02-23 23:14:16 +01:00
Bram Kragten
aa831a9adf bump marked and flatmap (#11814) 2022-02-23 22:11:13 +01:00
Bram Kragten
43d4f55392 Update workbox (#11813)
* Update workbox

* dedupe
2022-02-23 22:10:16 +01:00
Bram Kragten
130c66fb24 Update yarn (#11812)
* Update yarn

* Update yarn.lock

* dedupe
2022-02-23 22:08:43 +01:00
Zack Barett
684c232c8c Sensor Card Editor to Ha Form (#11810)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-23 21:46:52 +01:00
105 changed files with 3240 additions and 3622 deletions

File diff suppressed because one or more lines are too long

785
.yarn/releases/yarn-3.2.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.0.2.cjs
yarnPath: .yarn/releases/yarn-3.2.0.cjs

View File

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

View File

@@ -7,7 +7,7 @@ const source = require("vinyl-source-stream");
const vinylBuffer = require("vinyl-buffer");
const gulp = require("gulp");
const fs = require("fs");
const foreach = require("gulp-foreach");
const flatmap = require("gulp-flatmap");
const merge = require("gulp-merge-json");
const rename = require("gulp-rename");
const transform = require("gulp-json-transform");
@@ -183,7 +183,7 @@ gulp.task("build-merged-translations", () =>
})
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
.pipe(
foreach((stream, file) => {
flatmap((stream, file) => {
// For each language generate a merged json file. It begins with the master
// translation as a failsafe for untranslated strings, and merges all parent
// tags into one file for each specific subtag

View File

@@ -1,5 +1,5 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { LovelaceConfig } from "../../../../src/data/lovelace";
import { Lovelace } from "../../../../src/panels/lovelace/types";
@@ -20,6 +20,8 @@ class HcLovelace extends LitElement {
@property() public urlPath: string | null = null;
@query("hui-view") private _huiView?: HTMLElement;
protected render(): TemplateResult {
const index = this._viewIndex;
if (index === undefined) {
@@ -78,12 +80,12 @@ class HcLovelace extends LitElement {
this.lovelaceConfig.background;
if (configBackground) {
(this.shadowRoot!.querySelector(
"hui-view"
) as HTMLElement)!.style.setProperty(
this._huiView!.style.setProperty(
"--lovelace-background",
configBackground
);
} else {
this._huiView!.style.removeProperty("--lovelace-background");
}
}
}
@@ -116,6 +118,9 @@ class HcLovelace extends LitElement {
:host > * {
flex: 1;
}
hui-view {
background: var(--lovelace-background, var(--primary-background-color));
}
`;
}
}

View File

@@ -279,7 +279,7 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
can_play: true,
can_expand: false,
children_media_class: null,
thumbnail: null,
thumbnail: "https://brands.home-assistant.io/_/image/logo.png",
},
{
title: "movie.mp4",

View File

@@ -14,7 +14,7 @@ import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate";
import "../../../src/common/search/search-input";
import "../../../src/components/search-input";
import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-icon-button";
@@ -110,8 +110,6 @@ class HassioAddonStore extends LitElement {
<div class="search">
<search-input
.hass=${this.hass}
no-label-float
no-underline
.filter=${this._filter}
@value-changed=${this._filterChanged}
></search-input>

View File

@@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
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/search-input";
import { stringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
@@ -80,8 +80,6 @@ class HassioHardwareDialog extends LitElement {
></ha-icon-button>
<search-input
.hass=${this.hass}
dialogInitialFocus
no-label-float
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
.label=${this._dialogParams.supervisor.localize(

View File

@@ -106,6 +106,9 @@ class HassioRepositoriesDialog extends LitElement {
</paper-item-body>
<div class="delete">
<ha-icon-button
.label=${this._dialogParams!.supervisor.localize(
"dialog.repositories.remove"
)}
.disabled=${usedRepositories.includes(repo.slug)}
.slug=${repo.slug}
.path=${usedRepositories.includes(repo.slug)

View File

@@ -1,9 +1,12 @@
// Compat needs to be first import
import "../../src/resources/compatibility";
import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings";
import "../../src/resources/roboto";
import "../../src/resources/safari-14-attachshadow-patch";
import "./hassio-main";
setCancelSyntheticClickEvents(false);
const styleEl = document.createElement("style");
styleEl.innerHTML = `
body {

View File

@@ -45,7 +45,6 @@ import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage";
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
import { HomeAssistant, Route } from "../../../src/types";
import { addonArchIsSupported, extractChangelog } from "../util/addon";
@@ -57,6 +56,12 @@ declare global {
type updateType = "os" | "supervisor" | "core" | "addon";
const SUPERVISOR_UPDATE_NAMES = {
core: "Home Assistant Core",
os: "Home Assistant Operating System",
supervisor: "Home Assistant Supervisor",
};
const changelogUrl = (
entry: updateType,
version: string

View File

@@ -117,7 +117,7 @@
"leaflet-draw": "^1.0.4",
"lit": "^2.1.2",
"lit-vaadin-helpers": "^0.3.0",
"marked": "^3.0.2",
"marked": "^4.0.12",
"memoize-one": "^5.2.1",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "^0.3.2",
@@ -137,12 +137,12 @@
"vue": "^2.6.12",
"vue2-daterange-picker": "^0.5.1",
"web-animations-js": "^2.3.2",
"workbox-cacheable-response": "^6.1.5",
"workbox-core": "^6.1.5",
"workbox-expiration": "^6.1.5",
"workbox-precaching": "^6.1.5",
"workbox-routing": "^6.1.5",
"workbox-strategies": "^6.1.5",
"workbox-cacheable-response": "^6.4.2",
"workbox-core": "^6.4.2",
"workbox-expiration": "^6.4.2",
"workbox-precaching": "^6.4.2",
"workbox-routing": "^6.4.2",
"workbox-strategies": "^6.4.2",
"xss": "^1.0.9"
},
"devDependencies": {
@@ -171,7 +171,7 @@
"@types/js-yaml": "^4",
"@types/leaflet": "^1",
"@types/leaflet-draw": "^1",
"@types/marked": "^2",
"@types/marked": "^4",
"@types/mocha": "^8",
"@types/qrcode": "^1.4.2",
"@types/sortablejs": "^1",
@@ -198,7 +198,7 @@
"fs-extra": "^7.0.1",
"glob": "^7.2.0",
"gulp": "^4.0.2",
"gulp-foreach": "^0.1.0",
"gulp-flatmap": "^1.0.2",
"gulp-json-transform": "^0.4.6",
"gulp-merge-json": "^1.3.1",
"gulp-rename": "^2.0.0",
@@ -235,7 +235,7 @@
"webpack-dev-server": "^4.3.0",
"webpack-manifest-plugin": "^4.0.2",
"webpackbar": "^5.0.0-3",
"workbox-build": "^6.1.5"
"workbox-build": "^6.4.2"
},
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": {
@@ -255,5 +255,6 @@
"prettier": {
"trailingComma": "es5",
"arrowParens": "always"
}
},
"packageManager": "yarn@3.2.0"
}

View File

@@ -2,6 +2,6 @@
from pathlib import Path
def where():
def where() -> Path:
"""Return path to the frontend."""
return Path(__file__).parent

0
public/py.typed Normal file
View File

View File

@@ -1,6 +1,6 @@
[metadata]
name = home-assistant-frontend
version = 20220223.0
version = 20220301.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0
@@ -19,3 +19,8 @@ python_requires = >= 3.4.0
[options.packages.find]
include =
hass_frontend*
[mypy]
python_version = 3.4
show_error_codes = True
strict = True

View File

@@ -21,7 +21,7 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event";
import "../../common/search/search-input";
import "../search-input";
import { debounce } from "../../common/util/debounce";
import { nextRender } from "../../common/util/render-status";
import { haStyleScrollbar } from "../../resources/styles";

View File

@@ -115,6 +115,9 @@ class DateRangePickerElement extends WrappedElement {
color: var(--primary-text-color);
min-width: initial !important;
}
.daterangepicker:before {
display: none;
}
.daterangepicker:after {
border-bottom: 6px solid var(--card-background-color);
}

View File

@@ -1,6 +1,6 @@
import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { styles } from "@material/mwc-select/mwc-select.css";
import { html, nothing } from "lit";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status";
@@ -20,8 +20,6 @@ export class HaSelect extends SelectBase {
></span>`;
}
static override styles = [styles];
connectedCallback() {
super.connectedCallback();
window.addEventListener("translations-updated", this._translationsUpdated);
@@ -39,6 +37,15 @@ export class HaSelect extends SelectBase {
await nextRender();
this.layoutOptions();
}, 500);
static override styles = [
styles,
css`
.mdc-select:not(.mdc-select--disabled) .mdc-select__icon {
color: var(--secondary-text-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {

View File

@@ -51,10 +51,11 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
private _filterEntities = (entity: HassEntity): boolean => {
if (this.selector.entity?.domain) {
const filterDomain = this.selector.entity.domain;
const filterDomainIsArray = Array.isArray(filterDomain);
const entityDomain = computeStateDomain(entity);
if (
(Array.isArray(filterDomain) && !filterDomain.includes(entityDomain)) ||
entityDomain !== filterDomain
(filterDomainIsArray && !filterDomain.includes(entityDomain)) ||
(!filterDomainIsArray && entityDomain !== filterDomain)
) {
return false;
}

View File

@@ -12,6 +12,7 @@ import {
} from "../../data/media-player";
import type { MediaSelector, MediaSelectorValue } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
import "../ha-alert";
import "../ha-form/ha-form";
import type { HaFormSchema } from "../ha-form/types";
@@ -50,6 +51,18 @@ export class HaMediaSelector extends LitElement {
getSignedPath(this.hass, thumbnail).then((signedPath) => {
this._thumbnailUrl = signedPath.path;
});
} else if (
thumbnail &&
thumbnail.startsWith("https://brands.home-assistant.io")
) {
// The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon
this._thumbnailUrl = brandsUrl({
domain: extractDomainFromBrandUrl(thumbnail),
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
});
} else {
this._thumbnailUrl = thumbnail;
}

View File

@@ -42,9 +42,7 @@ export class HaTab extends LitElement {
@keydown=${this._handleKeyDown}
>
${this.narrow ? html`<slot name="icon"></slot>` : ""}
${!this.narrow || this.active
? html`<span class="name">${this.name}</span>`
: ""}
<span class="name">${this.name}</span>
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
</div>
`;

View File

@@ -53,6 +53,10 @@ export class HaTextField extends TextFieldBase {
padding-right: var(--text-field-suffix-padding-right, 0px);
}
.mdc-text-field__icon {
color: var(--secondary-text-color);
}
input {
text-align: var(--text-field-text-align);
}

View File

@@ -34,23 +34,24 @@ import {
MediaPickedEvent,
MediaPlayerBrowseAction,
} from "../../data/media-player";
import { browseLocalMediaPlayer } from "../../data/media_source";
import { isTTSMediaSource } from "../../data/tts";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
import { documentationUrl } from "../../util/documentation-url";
import "../entity/ha-entity-picker";
import "../ha-button-menu";
import "../ha-card";
import type { HaCard } from "../ha-card";
import "../ha-circular-progress";
import "../ha-fab";
import "../ha-icon-button";
import "../ha-svg-icon";
import "../ha-fab";
import { browseLocalMediaPlayer } from "../../data/media_source";
import { isTTSMediaSource } from "../../data/tts";
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
import "./ha-browse-media-tts";
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
declare global {
interface HASSDomEvents {
@@ -681,6 +682,17 @@ export class HaMediaPlayerBrowse extends LitElement {
// Thumbnails served by local API require authentication
const signedPath = await getSignedPath(this.hass, thumbnailUrl);
thumbnailUrl = signedPath.path;
} else if (
thumbnailUrl.startsWith("https://brands.home-assistant.io")
) {
// The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon
thumbnailUrl = brandsUrl({
domain: extractDomainFromBrandUrl(thumbnailUrl),
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
});
}
thumbnailCard.style.backgroundImage = `url(${thumbnailUrl})`;
observer.unobserve(thumbnailCard); // loaded, so no need to observe anymore

View File

@@ -1,12 +1,12 @@
import { mdiClose, mdiMagnify } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../components/ha-icon-button";
import "../../components/ha-svg-icon";
import "../../components/ha-textfield";
import type { HaTextField } from "../../components/ha-textfield";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../dom/fire_event";
import "./ha-icon-button";
import "./ha-svg-icon";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
import { HomeAssistant } from "../types";
import { fireEvent } from "../common/dom/fire_event";
@customElement("search-input")
class SearchInput extends LitElement {

View File

@@ -1,8 +1,13 @@
import { HomeAssistant } from "../types";
interface ValidationResult {
valid: boolean;
error: string | null;
interface ValidConfig {
valid: true;
error: null;
}
interface InvalidConfig {
valid: false;
error: string;
}
type ValidKeys = "trigger" | "action" | "condition";
@@ -12,7 +17,7 @@ export const validateConfig = <
>(
hass: HomeAssistant,
config: T
): Promise<Record<keyof T, ValidationResult>> =>
): Promise<Record<keyof T, ValidConfig | InvalidConfig>> =>
hass.callWS({
type: "validate_config",
...config,

View File

@@ -1,3 +1,4 @@
import { HassEntity } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
export interface InputDateTime {
@@ -17,6 +18,19 @@ export interface InputDateTimeMutableParams {
has_date: boolean;
}
export const stateToIsoDateString = (entityState: HassEntity) =>
`${entityState.attributes.year || "1970"}-${String(
entityState.attributes.month || "01"
).padStart(2, "0")}-${String(entityState.attributes.day || "01").padStart(
2,
"0"
)}T${String(entityState.attributes.hour || "00").padStart(2, "0")}:${String(
entityState.attributes.minute || "00"
).padStart(2, "0")}:${String(entityState.attributes.second || "00").padStart(
2,
"0"
)}`;
export const setInputDateTimeValue = (
hass: HomeAssistant,
entityId: string,

View File

@@ -1,7 +1,10 @@
import { HomeAssistant } from "../types";
import { Action } from "./script";
export const callExecuteScript = (hass: HomeAssistant, sequence: Action[]) =>
export const callExecuteScript = (
hass: HomeAssistant,
sequence: Action | Action[]
) =>
hass.callWS({
type: "execute_script",
sequence,

View File

@@ -1,58 +0,0 @@
import { HomeAssistant } from "../../types";
interface SupervisorBaseAvailableUpdates {
panel_path?: string;
update_type?: string;
version_latest?: string;
}
interface SupervisorAddonAvailableUpdates
extends SupervisorBaseAvailableUpdates {
update_type?: "addon";
icon?: string;
name?: string;
}
interface SupervisorCoreAvailableUpdates
extends SupervisorBaseAvailableUpdates {
update_type?: "core";
}
interface SupervisorOsAvailableUpdates extends SupervisorBaseAvailableUpdates {
update_type?: "os";
}
interface SupervisorSupervisorAvailableUpdates
extends SupervisorBaseAvailableUpdates {
update_type?: "supervisor";
}
export type SupervisorAvailableUpdates =
| SupervisorAddonAvailableUpdates
| SupervisorCoreAvailableUpdates
| SupervisorOsAvailableUpdates
| SupervisorSupervisorAvailableUpdates;
export interface SupervisorAvailableUpdatesResponse {
available_updates: SupervisorAvailableUpdates[];
}
export const fetchSupervisorAvailableUpdates = async (
hass: HomeAssistant
): Promise<SupervisorAvailableUpdates[]> =>
(
await hass.callWS<SupervisorAvailableUpdatesResponse>({
type: "supervisor/api",
endpoint: "/available_updates",
method: "get",
})
).available_updates;
export const refreshSupervisorAvailableUpdates = async (
hass: HomeAssistant
): Promise<void> =>
hass.callWS<void>({
type: "supervisor/api",
endpoint: "/refresh_updates",
method: "post",
});

37
src/data/update.ts Normal file
View File

@@ -0,0 +1,37 @@
import { HomeAssistant } from "../types";
export interface UpdateDescription {
identifier: string;
name: string;
domain: string;
current_version: string;
available_version: string;
changelog_content: string | null;
changelog_url: string | null;
icon_url: string | null;
supports_backup: boolean;
}
export interface SkipUpdateParams {
domain: string;
version: string;
identifier: string;
}
export interface PerformUpdateParams extends SkipUpdateParams {
backup?: boolean;
}
export const fetchUpdateInfo = (
hass: HomeAssistant
): Promise<UpdateDescription[]> => hass.callWS({ type: "update/info" });
export const skipUpdate = (
hass: HomeAssistant,
params: SkipUpdateParams
): Promise<void> => hass.callWS({ type: "update/skip", ...params });
export const performUpdate = (
hass: HomeAssistant,
params: PerformUpdateParams
): Promise<void> => hass.callWS({ type: "update/update", ...params });

View File

@@ -117,13 +117,17 @@ class DataEntryFlowDialog extends LitElement {
);
} catch (err: any) {
this.closeDialog();
let message = err.message || err.body || "Unknown error";
if (typeof message !== "string") {
message = JSON.stringify(message);
}
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: `${this.hass.localize(
"ui.panel.config.integrations.config_flow.could_not_load"
)}: ${err.message || err.body}`,
)}: ${message}`,
});
return;
}

View File

@@ -15,7 +15,7 @@ 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 "../../components/search-input";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { LocalizeFunc } from "../../common/translations/localize";
import "../../components/ha-icon-next";

View File

@@ -4,7 +4,10 @@ import { customElement, property } from "lit/decorators";
import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity";
import { setInputDateTimeValue } from "../../../data/input_datetime";
import {
setInputDateTimeValue,
stateToIsoDateString,
} from "../../../data/input_datetime";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-input_datetime")
@@ -24,7 +27,7 @@ class MoreInfoInputDatetime extends LitElement {
? html`
<ha-date-input
.locale=${this.hass.locale}
.value=${`${this.stateObj.attributes.year}-${this.stateObj.attributes.month}-${this.stateObj.attributes.day}`}
.value=${stateToIsoDateString(this.stateObj)}
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
@value-changed=${this._dateChanged}
>

View File

@@ -50,12 +50,11 @@ class MoreInfoMediaPlayer extends LitElement {
const controls = computeMediaControls(stateObj);
return html`
${!controls
? ""
: html`
<div class="controls">
<div class="basic-controls">
${controls!.map(
${!controls
? ""
: controls.map(
(control) => html`
<ha-icon-button
action=${control.action}
@@ -85,7 +84,6 @@ class MoreInfoMediaPlayer extends LitElement {
`
: ""}
</div>
`}
${(supportsFeature(stateObj, SUPPORT_VOLUME_SET) ||
supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS)) &&
![UNAVAILABLE, UNKNOWN, "off"].includes(stateObj.state)

View File

@@ -86,11 +86,11 @@ export class QuickBar extends LitElement {
@state() private _search = "";
@state() private _opened = false;
@state() private _open = false;
@state() private _commandMode = false;
@state() private _done = false;
@state() private _opened = false;
@state() private _narrow = false;
@@ -109,12 +109,12 @@ export class QuickBar extends LitElement {
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
this._initializeItemsIfNeeded();
this._opened = true;
this._open = true;
}
public closeDialog() {
this._open = false;
this._opened = false;
this._done = false;
this._focusSet = false;
this._filter = "";
this._search = "";
@@ -133,7 +133,7 @@ export class QuickBar extends LitElement {
);
protected render() {
if (!this._opened) {
if (!this._open) {
return html``;
}
@@ -218,7 +218,8 @@ export class QuickBar extends LitElement {
`
: html`
<mwc-list>
<lit-virtualizer
${this._opened
? html`<lit-virtualizer
scroller
@keydown=${this._handleListItemKeyDown}
@rangechange=${this._handleRangeChanged}
@@ -229,13 +230,14 @@ export class QuickBar extends LitElement {
? "calc(100vh - 56px)"
: `${Math.min(
items.length * (this._commandMode ? 56 : 72) + 26,
this._done ? 500 : 0
500
)}px`,
})}
.items=${items}
.renderItem=${this._renderItem}
>
</lit-virtualizer>
</lit-virtualizer>`
: ""}
</mwc-list>
`}
${this._hint ? html`<div class="hint">${this._hint}</div>` : ""}
@@ -252,9 +254,7 @@ export class QuickBar extends LitElement {
}
private _handleOpened() {
this.updateComplete.then(() => {
this._done = true;
});
this._opened = true;
}
private async _handleRangeChanged(e) {
@@ -454,9 +454,10 @@ export class QuickBar extends LitElement {
}
private _handleItemClick(ev) {
const listItem = ev.target.closest("mwc-list-item");
this.processItemAndCloseDialog(
(ev.target as any).item,
Number((ev.target as HTMLElement).getAttribute("index"))
listItem.item,
Number(listItem.getAttribute("index"))
);
}

View File

@@ -0,0 +1,211 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTL } from "../../common/util/compute_rtl";
import "../../components/ha-alert";
import "../../components/ha-checkbox";
import "../../components/ha-circular-progress";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-faded";
import "../../components/ha-formfield";
import "../../components/ha-icon-button";
import "../../components/ha-markdown";
import {
performUpdate,
skipUpdate,
UpdateDescription,
} from "../../data/update";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { UpdateDialogParams } from "./show-ha-update-dialog";
@customElement("ha-update-dialog")
export class HaUpdateDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _opened = false;
@state() private _updating = false;
@state() private _error?: string;
@state() private _update!: UpdateDescription;
_refreshCallback!: () => void;
public async showDialog(dialogParams: UpdateDialogParams): Promise<void> {
this._opened = true;
this._update = dialogParams.update;
this._refreshCallback = dialogParams.refreshCallback;
}
public async closeDialog(): Promise<void> {
this._opened = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._opened) {
return html``;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.updates.dialog.title", {
name: this._update.name,
})
)}
>
<div>
${this._error
? html`<ha-alert alert-type="error" .rtl=${computeRTL(this.hass)}>
${this._error}
</ha-alert>`
: ""}
${!this._updating
? html`
${this._update.changelog_content
? html`
<ha-faded>
<ha-markdown .content=${this._update.changelog_content}>
</ha-markdown>
</ha-faded>
`
: ""}
${this._update.changelog_url
? html`<a href=${this._update.changelog_url} target="_blank">
Full changelog
</a> `
: ""}
<p>
${this.hass.localize(
"ui.panel.config.updates.dialog.description",
{
name: this._update.name,
version: this._update.current_version,
newest_version: this._update.available_version,
}
)}
</p>
${this._update.supports_backup
? html`
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.updates.dialog.create_backup"
)}
>
<ha-checkbox checked></ha-checkbox>
</ha-formfield>
`
: ""}
`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this.hass.localize(
"ui.panel.config.updates.dialog.updating",
{
name: this._update.name,
version: this._update.available_version,
}
)}
</p>`}
</div>
${!this._updating
? html`
<mwc-button slot="secondaryAction" @click=${this._skipUpdate}>
${this.hass.localize("ui.common.skip")}
</mwc-button>
<mwc-button
.disabled=${this._updating}
slot="primaryAction"
@click=${this._performUpdate}
>
${this.hass.localize("ui.panel.config.updates.dialog.update")}
</mwc-button>
`
: ""}
</ha-dialog>
`;
}
get _shouldCreateBackup(): boolean {
if (!this._update.supports_backup) {
return false;
}
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
if (checkbox) {
return checkbox.checked;
}
return true;
}
private async _performUpdate() {
this._error = undefined;
this._updating = true;
try {
await performUpdate(this.hass, {
domain: this._update.domain,
identifier: this._update.identifier,
version: this._update.available_version,
backup: this._shouldCreateBackup,
});
} catch (err: any) {
this._error = err.message;
this._updating = false;
return;
}
this._updating = false;
this._refreshCallback();
this.closeDialog();
}
private async _skipUpdate() {
this._error = undefined;
try {
await skipUpdate(this.hass, {
domain: this._update.domain,
identifier: this._update.identifier,
version: this._update.available_version,
});
} catch (err: any) {
this._error = err.message;
return;
}
this._refreshCallback();
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
ha-markdown {
padding-bottom: 8px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-update-dialog": HaUpdateDialog;
}
}

View File

@@ -0,0 +1,18 @@
import { fireEvent } from "../../common/dom/fire_event";
import { UpdateDescription } from "../../data/update";
export interface UpdateDialogParams {
update: UpdateDescription;
refreshCallback: () => void;
}
export const showUpdateDialog = (
element: HTMLElement,
dialogParams: UpdateDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "ha-update-dialog",
dialogImport: () => import("./ha-update-dialog"),
dialogParams,
});
};

View File

@@ -13,6 +13,9 @@ export const SubscribeMixin = <T extends Constructor<ReactiveElement>>(
class SubscribeClass extends superClass {
@property({ attribute: false }) public hass?: HomeAssistant;
// we wait with subscribing till these properties are set on the host element
protected hassSubscribeRequiredHostProps?: string[];
private __unsubs?: Array<UnsubscribeFunc | Promise<UnsubscribeFunc>>;
public connectedCallback() {
@@ -39,6 +42,16 @@ export const SubscribeMixin = <T extends Constructor<ReactiveElement>>(
super.updated(changedProps);
if (changedProps.has("hass")) {
this.__checkSubscribed();
return;
}
if (!this.hassSubscribeRequiredHostProps) {
return;
}
for (const key of changedProps.keys()) {
if (this.hassSubscribeRequiredHostProps.includes(key as string)) {
this.__checkSubscribed();
return;
}
}
}
@@ -52,7 +65,10 @@ export const SubscribeMixin = <T extends Constructor<ReactiveElement>>(
if (
this.__unsubs !== undefined ||
!(this as unknown as Element).isConnected ||
this.hass === undefined
this.hass === undefined ||
this.hassSubscribeRequiredHostProps?.some(
(prop) => this[prop] === undefined
)
) {
return;
}

View File

@@ -16,10 +16,16 @@ import "../../../../components/ha-icon-button";
import "../../../../components/ha-select";
import type { HaSelect } from "../../../../components/ha-select";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { validateConfig } from "../../../../data/config";
import { Action, getActionType } from "../../../../data/script";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { callExecuteScript } from "../../../../data/service";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import "./types/ha-automation-action-activate_scene";
import "./types/ha-automation-action-choose";
import "./types/ha-automation-action-condition";
@@ -180,6 +186,11 @@ export default class HaAutomationActionRow extends LitElement {
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.run_action"
)}
</mwc-list-item>
<mwc-list-item .disabled=${!this._uiModeAvailable}>
${yamlMode
? this.hass.localize(
@@ -290,17 +301,54 @@ export default class HaAutomationActionRow extends LitElement {
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._switchYamlMode();
this._runAction();
break;
case 1:
fireEvent(this, "duplicate");
this._switchYamlMode();
break;
case 2:
fireEvent(this, "duplicate");
break;
case 3:
this._onDelete();
break;
}
}
private async _runAction() {
const validated = await validateConfig(this.hass, {
action: this.action,
});
if (!validated.action.valid) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.actions.invalid_action"
),
text: validated.action.error,
});
return;
}
try {
await callExecuteScript(this.hass, this.action);
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.actions.run_action_error"
),
text: err.message || err,
});
return;
}
showToast(this, {
message: this.hass.localize(
"ui.panel.config.automation.editor.actions.run_action_success"
),
});
}
private _onDelete() {
showConfirmationDialog(this, {
text: this.hass.localize(

View File

@@ -145,7 +145,7 @@ export class HaDeviceAction extends LitElement {
static styles = css`
ha-device-picker {
display: block;
margin-bottom: 24px;
margin-bottom: 16px;
}
ha-device-action-picker {
display: block;

View File

@@ -50,7 +50,7 @@ export class HaEventAction extends LitElement implements ActionElement {
<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.event.service_data"
"ui.panel.config.automation.editor.actions.type.event.event_data"
)}
.name=${"event_data"}
.defaultValue=${event_data}

View File

@@ -162,8 +162,8 @@ export class HaRepeatAction extends LitElement implements ActionElement {
return [
haStyle,
css`
ha-select {
margin-top: 8px;
ha-textfield {
margin-top: 16px;
}
`,
];

View File

@@ -33,7 +33,6 @@ export class HaWaitForTriggerAction
.value=${timeout || ""}
@change=${this._valueChanged}
></ha-textfield>
<br />
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_for_trigger.continue_timeout"

View File

@@ -117,8 +117,8 @@ export class HaTimeCondition extends LitElement implements ConditionElement {
);
const data = {
mode_before: "value",
mode_after: "value",
mode_before: inputModeBefore ? "input" : "value",
mode_after: inputModeAfter ? "input" : "value",
...this.condition,
};
@@ -137,18 +137,11 @@ export class HaTimeCondition extends LitElement implements ConditionElement {
ev.stopPropagation();
const newValue = ev.detail.value;
const newModeAfter = newValue.mode_after === "input";
const newModeBefore = newValue.mode_before === "input";
this._inputModeAfter = newValue.mode_after === "input";
this._inputModeBefore = newValue.mode_before === "input";
if (newModeAfter !== this._inputModeAfter) {
this._inputModeAfter = newModeAfter;
newValue.after = undefined;
}
if (newModeBefore !== this._inputModeBefore) {
this._inputModeBefore = newModeBefore;
newValue.before = undefined;
}
delete newValue.mode_after;
delete newValue.mode_before;
Object.keys(newValue).forEach((key) =>
newValue[key] === undefined || newValue[key] === ""

View File

@@ -346,7 +346,8 @@ export class HaManualAutomationEditor extends LitElement {
.link-button-row {
padding: 14px;
}
ha-textarea {
ha-textarea,
ha-textfield {
display: block;
}
span[slot="introduction"] a {

View File

@@ -23,9 +23,9 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
private _schema = memoizeOne(
(localize: LocalizeFunc, inputMode?: boolean): HaFormSchema[] => {
const modeSchema = inputMode
? { name: "at", selector: { entity: { domain: "input_datetime" } } }
: { name: "at", selector: { time: {} } };
const atSelector = inputMode
? { entity: { domain: "input_datetime" } }
: { time: {} };
return [
{
@@ -47,7 +47,7 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
],
],
},
modeSchema,
{ name: "at", selector: atSelector },
];
}
);
@@ -80,7 +80,7 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
const schema: HaFormSchema[] = this._schema(this.hass.localize, inputMode);
const data = {
mode: "value",
mode: inputMode ? "input" : "value",
...this.trigger,
};
@@ -99,7 +99,8 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
ev.stopPropagation();
const newValue = ev.detail.value;
this._inputMode = newValue.mode.value === "input";
this._inputMode = newValue.mode === "input";
delete newValue.mode;
Object.keys(newValue).forEach((key) =>
newValue[key] === undefined || newValue[key] === ""

View File

@@ -29,6 +29,7 @@ import "./cloud-remote-pref";
import "./cloud-tts-pref";
import "./cloud-webhooks";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
@customElement("cloud-account")
export class CloudAccount extends SubscribeMixin(LitElement) {
@@ -276,10 +277,21 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.cloud.account.sign_out_confirm"
),
confirmText: this.hass!.localize("ui.common.yes"),
dismissText: this.hass!.localize("ui.common.no"),
confirm: () => this._logoutFromCloud(),
});
}
}
private async _logoutFromCloud() {
await cloudLogout(this.hass);
fireEvent(this, "ha-refresh-cloud-status");
}
}
_computeRTLDirection(hass) {
return computeRTLDirection(hass);

View File

@@ -40,7 +40,7 @@ class ConfigNameForm extends LitElement {
)}
.disabled=${disabled}
.value=${this._nameValue}
@changed=${this._handleChange}
@change=${this._handleChange}
></ha-textfield>
</div>
<div class="card-actions">

View File

@@ -17,7 +17,7 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
@@ -26,10 +26,6 @@ import "../../../components/ha-menu-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-svg-icon";
import { CloudStatus } from "../../../data/cloud";
import {
refreshSupervisorAvailableUpdates,
SupervisorAvailableUpdates,
} from "../../../data/supervisor/root";
import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
import "../../../layouts/ha-app-layout";
import { haStyle } from "../../../resources/styles";
@@ -38,10 +34,70 @@ import "../ha-config-section";
import { configSections } from "../ha-panel-config";
import "./ha-config-navigation";
import "./ha-config-updates";
import { fireEvent } from "../../../common/dom/fire_event";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { showToast } from "../../../util/toast";
import { documentationUrl } from "../../../util/documentation-url";
import { UpdateDescription } from "../../../data/update";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeRTL } from "../../../common/util/compute_rtl";
const randomTip = (hass: HomeAssistant) => {
const weighted: string[] = [];
const tips = [
{
content: hass.localize(
"ui.panel.config.tips.join",
"forums",
html`<a
href="https://community.home-assistant.io"
target="_blank"
rel="noreferrer"
>Forums</a
>`,
"twitter",
html`<a
href=${documentationUrl(hass, `/twitter`)}
target="_blank"
rel="noreferrer"
>Twitter</a
>`,
"discord",
html`<a
href=${documentationUrl(hass, `/join-chat`)}
target="_blank"
rel="noreferrer"
>Chat</a
>`,
"blog",
html`<a
href=${documentationUrl(hass, `/blog`)}
target="_blank"
rel="noreferrer"
>Blog</a
>`,
"newsletter",
html`<span class="keep-together"
><a
href=${documentationUrl(hass, `/newsletter`)}
target="_blank"
rel="noreferrer"
>Newsletter</a
>
<ha-svg-icon class="new" .path=${mdiNewBox}></ha-svg-icon
></span>`
),
weight: 2,
},
{ content: hass.localize("ui.dialogs.quick-bar.key_c_hint"), weight: 1 },
];
tips.forEach((tip) => {
for (let i = 0; i < tip.weight; i++) {
weighted.push(tip.content);
}
});
return weighted[Math.floor(Math.random() * weighted.length)];
};
@customElement("ha-config-dashboard")
class HaConfigDashboard extends LitElement {
@@ -55,11 +111,11 @@ class HaConfigDashboard extends LitElement {
@property() public cloudStatus?: CloudStatus;
// null means not available
@property() public supervisorUpdates?: SupervisorAvailableUpdates[] | null;
@property() public updates?: UpdateDescription[] | null;
@property() public showAdvanced!: boolean;
private _notifyUpdates = false;
@state() private _tip?: string;
protected render(): TemplateResult {
return html`
@@ -72,6 +128,7 @@ class HaConfigDashboard extends LitElement {
></ha-menu-button>
<div main-title>${this.hass.localize("panel.config")}</div>
<ha-icon-button
.label=${this.hass.localize("ui.dialogs.quick-bar.title")}
.path=${mdiMagnify}
@click=${this._showQuickBar}
></ha-icon-button>
@@ -98,20 +155,23 @@ class HaConfigDashboard extends LitElement {
.isWide=${this.isWide}
full-width
>
${this.supervisorUpdates === undefined
? // Hide everything until updates loaded
html``
: html`${this.supervisorUpdates?.length
${this.updates === undefined
? html`<ha-alert .rtl=${computeRTL(this.hass)}>
${this.hass.localize(
"ui.panel.config.updates.checking_updates"
)}
</ha-alert>`
: this.updates?.length
? html`<ha-card>
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.supervisorUpdates=${this.supervisorUpdates}
.updates=${this.updates}
></ha-config-updates>
</ha-card>`
: ""}
<ha-card>
${this.narrow && this.supervisorUpdates?.length
${this.narrow && this.updates?.length
? html`<div class="title">
${this.hass.localize("panel.config")}
</div>`
@@ -141,51 +201,11 @@ class HaConfigDashboard extends LitElement {
.showAdvanced=${this.showAdvanced}
.pages=${configSections.dashboard}
></ha-config-navigation>
</ha-card>`}
</ha-card>
<div class="tips">
<ha-svg-icon .path=${mdiLightbulbOutline}></ha-svg-icon>
<span class="tip-word">Tip!</span>
<span class="text">
${this.hass.localize(
"ui.panel.config.tips.join",
"forums",
html`<a
href="https://community.home-assistant.io"
target="_blank"
rel="noreferrer"
>Forums</a
>`,
"twitter",
html`<a
href=${documentationUrl(this.hass, `/twitter`)}
target="_blank"
rel="noreferrer"
>Twitter</a
>`,
"discord",
html`<a
href=${documentationUrl(this.hass, `/join-chat`)}
target="_blank"
rel="noreferrer"
>Chat</a
>`,
"blog",
html`<a
href=${documentationUrl(this.hass, `/blog`)}
target="_blank"
rel="noreferrer"
>Blog</a
>`,
"newsletter",
html`<a
href=${documentationUrl(this.hass, `/newsletter`)}
target="_blank"
rel="noreferrer"
>Newsletter</a
>
<ha-svg-icon class="new" .path=${mdiNewBox}></ha-svg-icon>`
)}
</span>
<span class="text">${this._tip}</span>
</div>
</ha-config-section>
</ha-app-layout>
@@ -195,20 +215,8 @@ class HaConfigDashboard extends LitElement {
protected override updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!changedProps.has("supervisorUpdates") || !this._notifyUpdates) {
return;
}
this._notifyUpdates = false;
if (this.supervisorUpdates?.length) {
showToast(this, {
message: this.hass.localize(
"ui.panel.config.updates.updates_refreshed"
),
});
} else {
showToast(this, {
message: this.hass.localize("ui.panel.config.updates.no_new_updates"),
});
if (!this._tip && changedProps.has("hass")) {
this._tip = randomTip(this.hass);
}
}
@@ -222,18 +230,16 @@ class HaConfigDashboard extends LitElement {
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
if (isComponentLoaded(this.hass, "hassio")) {
this._notifyUpdates = true;
await refreshSupervisorAvailableUpdates(this.hass);
fireEvent(this, "ha-refresh-supervisor");
if (isComponentLoaded(this.hass, "update")) {
fireEvent(this, "ha-refresh-updates");
return;
}
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.updates.check_unavailable.title"
"ui.panel.config.updates.update_not_loaded.title"
),
text: this.hass.localize(
"ui.panel.config.updates.check_unavailable.description"
"ui.panel.config.updates.update_not_loaded.description"
),
warning: true,
});
@@ -293,6 +299,10 @@ class HaConfigDashboard extends LitElement {
.new {
color: var(--primary-color);
}
.keep-together {
display: inline-block;
}
`,
];
}

View File

@@ -1,21 +1,48 @@
import "@material/mwc-button/mwc-button";
import { mdiPackageVariant } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
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 "../../../components/ha-alert";
import "../../../components/ha-icon-next";
import "../../../components/ha-logo-svg";
import "../../../components/ha-svg-icon";
import { SupervisorAvailableUpdates } from "../../../data/supervisor/root";
import { UpdateDescription } from "../../../data/update";
import { showUpdateDialog } from "../../../dialogs/update-dialog/show-ha-update-dialog";
import { HomeAssistant } from "../../../types";
import "../../../components/ha-icon-next";
import { brandsUrl } from "../../../util/brands-url";
export const SUPERVISOR_UPDATE_NAMES = {
core: "Home Assistant Core",
os: "Home Assistant Operating System",
supervisor: "Home Assistant Supervisor",
};
const sortUpdates = memoizeOne((a: UpdateDescription, b: UpdateDescription) => {
if (a.domain === "hassio" && b.domain === "hassio") {
if (a.identifier === "core") {
return -1;
}
if (b.identifier === "core") {
return 1;
}
if (a.identifier === "supervisor") {
return -1;
}
if (b.identifier === "supervisor") {
return 1;
}
if (a.identifier === "os") {
return -1;
}
if (b.identifier === "os") {
return 1;
}
}
if (a.domain === "hassio") {
return -1;
}
if (b.domain === "hassio") {
return 1;
}
return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1;
});
@customElement("ha-config-updates")
class HaConfigUpdates extends LitElement {
@@ -24,62 +51,62 @@ class HaConfigUpdates extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false })
public supervisorUpdates?: SupervisorAvailableUpdates[] | null;
public updates?: UpdateDescription[] | null;
@state() private _showAll = false;
protected render(): TemplateResult {
if (!this.supervisorUpdates?.length) {
if (!this.updates?.length) {
return html``;
}
// Make sure the first updates shown are for the Supervisor
const sortedUpdates = this.updates.sort((a, b) => sortUpdates(a, b));
const updates =
this._showAll || this.supervisorUpdates.length <= 3
? this.supervisorUpdates
: this.supervisorUpdates.slice(0, 2);
this._showAll || sortedUpdates.length <= 3
? sortedUpdates
: sortedUpdates.slice(0, 2);
return html`
<div class="title">
${this.hass.localize("ui.panel.config.updates.title", {
count: this.supervisorUpdates.length,
count: sortedUpdates.length,
})}
</div>
${updates.map(
(update) => html`
<a href="/hassio${update.panel_path}">
<paper-icon-item>
<paper-icon-item @click=${this._showUpdate} .update=${update}>
<span slot="item-icon" class="icon">
${update.update_type === "addon"
? update.icon
? html`<img src="/api/hassio${update.icon}" />`
: html`<ha-svg-icon
.path=${mdiPackageVariant}
></ha-svg-icon>`
: html`<ha-logo-svg></ha-logo-svg>`}
<img
src=${update.icon_url ||
brandsUrl({
domain: update.domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
/>
</span>
<paper-item-body two-line>
${update.update_type === "addon"
? update.name
: SUPERVISOR_UPDATE_NAMES[update.update_type!]}
${update.name}
<div secondary>
${this.hass.localize(
"ui.panel.config.updates.version_available",
{
version_available: update.version_latest,
version_available: update.available_version,
}
)}
</div>
</paper-item-body>
${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""}
</paper-icon-item>
</a>
`
)}
${!this._showAll && this.supervisorUpdates.length >= 4
${!this._showAll && this.updates.length >= 4
? html`
<button class="show-more" @click=${this._showAllClicked}>
${this.hass.localize("ui.panel.config.updates.more_updates", {
count: this.supervisorUpdates!.length - updates.length,
count: this.updates!.length - updates.length,
})}
</button>
`
@@ -91,6 +118,14 @@ class HaConfigUpdates extends LitElement {
this._showAll = true;
}
private _showUpdate(ev) {
const update = ev.currentTarget.update as UpdateDescription;
showUpdateDialog(this, {
update,
refreshCallback: () => fireEvent(this, "ha-refresh-updates"),
});
}
static get styles(): CSSResultGroup[] {
return [
css`
@@ -139,6 +174,9 @@ class HaConfigUpdates extends LitElement {
outline: none;
text-decoration: underline;
}
paper-icon-item {
cursor: pointer;
}
`,
];
}

View File

@@ -438,6 +438,13 @@ export class HaConfigDeviceDashboard extends LitElement {
)}
.path=${mdiFilterVariant}
></ha-icon-button>
${this.narrow && activeFilters?.length
? html`<mwc-list-item @click=${this._clearFilter}
>${this.hass.localize("ui.components.data-table.filtering_by")}
${activeFilters.join(", ")}
<span class="clear">Clear</span></mwc-list-item
>`
: ""}
<ha-check-list-item
left
@request-selected=${this._showDisabledChanged}
@@ -523,6 +530,11 @@ export class HaConfigDeviceDashboard extends LitElement {
ha-button-menu {
margin-left: 8px;
}
.clear {
color: var(--primary-color);
padding-left: 8px;
text-transform: uppercase;
}
`,
haStyle,
];

View File

@@ -86,6 +86,7 @@ export class EnergyDeviceSettings extends LitElement {
: device.stat_consumption}</span
>
<ha-icon-button
.label=${this.hass.localize("ui.common.delete")}
@click=${this._deleteDevice}
.device=${device}
.path=${mdiDelete}

View File

@@ -585,6 +585,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
)}
.path=${mdiFilterVariant}
></ha-icon-button>
${this.narrow && activeFilters?.length
? html`<mwc-list-item @click=${this._clearFilter}
>${this.hass.localize(
"ui.components.data-table.filtering_by"
)}
${activeFilters.join(", ")}
<span class="clear">Clear</span></mwc-list-item
>`
: ""}
<ha-check-list-item
@request-selected=${this._showDisabledChanged}
.selected=${this._showDisabled}
@@ -896,6 +905,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
ha-button-menu {
margin-left: 8px;
}
.clear {
color: var(--primary-color);
padding-left: 8px;
text-transform: uppercase;
}
`,
];
}

View File

@@ -27,20 +27,18 @@ import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { listenMediaQuery } from "../../common/dom/media_query";
import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
import {
fetchSupervisorAvailableUpdates,
SupervisorAvailableUpdates,
} from "../../data/supervisor/root";
import { fetchUpdateInfo, UpdateDescription } from "../../data/update";
import "../../layouts/hass-loading-screen";
import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../types";
import { showToast } from "../../util/toast";
declare global {
// for fire event
interface HASSDomEvents {
"ha-refresh-cloud-status": undefined;
"ha-refresh-supervisor": undefined;
"ha-refresh-updates": undefined;
}
}
@@ -407,7 +405,7 @@ class HaPanelConfig extends HassRouterPage {
@state() private _cloudStatus?: CloudStatus;
@state() private _supervisorUpdates?: SupervisorAvailableUpdates[] | null;
@state() private _updates?: UpdateDescription[] | null;
private _listeners: Array<() => void> = [];
@@ -443,18 +441,18 @@ class HaPanelConfig extends HassRouterPage {
}
});
}
if (isComponentLoaded(this.hass, "hassio")) {
this._loadSupervisorUpdates();
this.addEventListener("ha-refresh-supervisor", () => {
this._loadSupervisorUpdates();
if (isComponentLoaded(this.hass, "update")) {
this._loadUpdates();
this.addEventListener("ha-refresh-updates", () => {
this._loadUpdates();
});
this.addEventListener("connection-status", (ev) => {
if (ev.detail === "connected") {
this._loadSupervisorUpdates();
this._loadUpdates();
}
});
} else {
this._supervisorUpdates = null;
this._updates = null;
}
this.addEventListener("ha-refresh-cloud-status", () =>
this._updateCloudStatus()
@@ -486,7 +484,7 @@ class HaPanelConfig extends HassRouterPage {
isWide,
narrow: this.narrow,
cloudStatus: this._cloudStatus,
supervisorUpdates: this._supervisorUpdates,
updates: this._updates,
});
} else {
el.route = this.routeTail;
@@ -495,7 +493,7 @@ class HaPanelConfig extends HassRouterPage {
el.isWide = isWide;
el.narrow = this.narrow;
el.cloudStatus = this._cloudStatus;
el.supervisorUpdates = this._supervisorUpdates;
el.updates = this._updates;
}
}
@@ -514,13 +512,33 @@ class HaPanelConfig extends HassRouterPage {
}
}
private async _loadSupervisorUpdates(): Promise<void> {
private async _loadUpdates(): Promise<void> {
const _showToast = this._updates !== undefined;
if (_showToast) {
showToast(this, {
message: this.hass.localize("ui.panel.config.updates.checking_updates"),
});
}
try {
this._supervisorUpdates = await fetchSupervisorAvailableUpdates(
this.hass
);
this._updates = await fetchUpdateInfo(this.hass);
} catch (err) {
this._supervisorUpdates = null;
this._updates = null;
}
if (_showToast) {
if (this._updates?.length) {
showToast(this, {
message: this.hass.localize(
"ui.panel.config.updates.updates_refreshed"
),
});
} else {
showToast(this, {
message: this.hass.localize("ui.panel.config.updates.no_new_updates"),
});
}
}
}
}

View File

@@ -15,7 +15,7 @@ import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import "../../../common/search/search-input";
import "../../../components/search-input";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import type { LocalizeFunc } from "../../../common/translations/localize";
import { extractSearchParam } from "../../../common/url/search-params";

View File

@@ -213,11 +213,11 @@ export class HaIntegrationCard extends LitElement {
} else {
stateTextExtra = html`
<br />
<a href="/config/logs"
>${this.hass.localize(
<a href=${`/config/logs/?filter=${item.domain}`}>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.check_the_logs"
)}</a
>
)}
</a>
`;
}
}

View File

@@ -8,7 +8,7 @@ import {
Node,
} from "vis-network/peer/esm/vis-network";
import { navigate } from "../../../../../common/navigate";
import "../../../../../common/search/search-input";
import "../../../../../components/search-input";
import "../../../../../components/device/ha-device-picker";
import "../../../../../components/ha-button-menu";
import "../../../../../components/ha-checkbox";
@@ -144,8 +144,6 @@ export class ZHANetworkVisualizationPage extends LitElement {
<div slot="header">
<search-input
.hass=${this.hass}
no-label-float
no-underline
class="header"
@value-changed=${this._handleSearchChange}
.filter=${this._filter}
@@ -161,8 +159,6 @@ export class ZHANetworkVisualizationPage extends LitElement {
${!this.narrow
? html`<search-input
.hass=${this.hass}
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
.filter=${this._filter}
.label=${this.hass.localize(

View File

@@ -347,7 +347,7 @@ class ZWaveJSConfigDashboard extends LitElement {
} else {
stateTextExtra = html`
<br />
<a href="/config/logs"
<a href="/config/logs?filter=zwave_js"
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.check_the_logs"
)}</a

View File

@@ -4,7 +4,7 @@ import "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import "../../../common/search/search-input";
import "../../../components/search-input";
import { extractSearchParam } from "../../../common/url/search-params";
import "./error-log-card";
import "./system-log-card";
@@ -22,7 +22,7 @@ export class HaConfigLogs extends LitElement {
@property() public route!: Route;
@state() private _filter = extractSearchParam("filter") ?? "";
@state() private _filter = extractSearchParam("filter") || "";
@query("system-log-card", true) private systemLog?: SystemLogCard;
@@ -43,8 +43,6 @@ export class HaConfigLogs extends LitElement {
<div slot="header">
<search-input
class="header"
no-label-float
no-underline
@value-changed=${this._filterChanged}
.hass=${this.hass}
.filter=${this._filter}
@@ -55,9 +53,6 @@ export class HaConfigLogs extends LitElement {
: html`
<div class="search">
<search-input
autofocus
no-label-float
no-underline
@value-changed=${this._filterChanged}
.hass=${this.hass}
.filter=${this._filter}

View File

@@ -18,6 +18,7 @@ import "../../../components/ha-code-editor";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-checkbox";
import "../../../components/search-input";
import "../../../components/ha-expansion-panel";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { EventsMixin } from "../../../mixins/events-mixin";
@@ -85,7 +86,8 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
padding: 0;
}
.filters ha-textfield {
.filters search-input {
display: block;
--mdc-text-field-fill-color: transparent;
}
@@ -252,28 +254,27 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
</tr>
<tr class="filters">
<th>
<ha-textfield
<search-input
label="[[localize('ui.panel.developer-tools.tabs.states.filter_entities')]]"
type="search"
value="[[_entityFilter]]"
on-input="_entityFilterChanged"
></ha-textfield>
on-value-changed="_entityFilterChanged"
></search-input>
</th>
<th>
<ha-textfield
<search-input
label="[[localize('ui.panel.developer-tools.tabs.states.filter_states')]]"
type="search"
value="[[_stateFilter]]"
on-input="_stateFilterChanged"
></ha-textfield>
on-value-changed="_stateFilterChanged"
></search-input>
</th>
<th hidden$="[[!computeShowAttributes(narrow, _showAttributes)]]">
<ha-textfield
<search-input
label="[[localize('ui.panel.developer-tools.tabs.states.filter_attributes')]]"
type="search"
value="[[_attributeFilter]]"
on-input="_attributeFilterChanged"
></ha-textfield>
on-value-changed="_attributeFilterChanged"
></search-input>
</th>
</tr>
<tr hidden$="[[!computeShowEntitiesPlaceholder(_entities)]]">
@@ -440,15 +441,15 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
}
_entityFilterChanged(ev) {
this._entityFilter = ev.target.value;
this._entityFilter = ev.detail.value;
}
_stateFilterChanged(ev) {
this._stateFilter = ev.target.value;
this._stateFilter = ev.detail.value;
}
_attributeFilterChanged(ev) {
this._attributeFilter = ev.target.value;
this._attributeFilter = ev.detail.value;
}
_getHistoryURL(entityId, inputDate) {

View File

@@ -289,6 +289,12 @@ class HaPanelDevTemplate extends LitElement {
.rendered.error {
color: var(--error-color);
}
@media all and (max-width: 870px) {
.render-pane {
max-width: 100%;
}
}
`,
];
}

View File

@@ -32,6 +32,8 @@ class HuiEnergyCarbonGaugeCard
@state() private _data?: EnergyData;
protected hassSubscribeRequiredHostProps = ["_config"];
public getCardSize(): number {
return 4;
}

View File

@@ -49,6 +49,8 @@ export class HuiEnergyDevicesGraphCard
@query("ha-chart-base") private _chart?: HaChartBase;
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
return [
getEnergyDataCollection(this.hass, {

View File

@@ -43,6 +43,8 @@ class HuiEnergyDistrubutionCard
@state() private _data?: EnergyData;
protected hassSubscribeRequiredHostProps = ["_config"];
public setConfig(config: EnergyDistributionCardConfig): void {
this._config = config;
}

View File

@@ -62,6 +62,8 @@ export class HuiEnergyGasGraphCard
@state() private _unit?: string;
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
return [
getEnergyDataCollection(this.hass, {

View File

@@ -35,6 +35,8 @@ class HuiEnergyGridGaugeCard
@state() private _data?: EnergyData;
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
return [
getEnergyDataCollection(this.hass!, {

View File

@@ -30,6 +30,8 @@ class HuiEnergySolarGaugeCard
@state() private _data?: EnergyData;
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
return [
getEnergyDataCollection(this.hass!, {

View File

@@ -61,6 +61,8 @@ export class HuiEnergySolarGraphCard
@state() private _end = endOfToday();
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
return [
getEnergyDataCollection(this.hass, {

View File

@@ -45,6 +45,8 @@ export class HuiEnergySourcesTableCard
@state() private _data?: EnergyData;
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
return [
getEnergyDataCollection(this.hass, {

View File

@@ -50,6 +50,8 @@ export class HuiEnergyUsageGraphCard
@state() private _end = endOfToday();
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
return [
getEnergyDataCollection(this.hass, {

View File

@@ -15,7 +15,7 @@ import { styleMap } from "lit/directives/style-map";
import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../common/search/search-input";
import "../../../../components/search-input";
import "../../../../components/ha-circular-progress";
import { UNAVAILABLE_STATES } from "../../../../data/entity";
import type {

View File

@@ -90,11 +90,9 @@ export class HuiAlarmPanelCardEditor
private _computeLabelCallback = (schema: HaFormSchema) => {
if (schema.name === "entity") {
return `${this.hass!.localize(
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.required"
)})`;
);
}
if (schema.name === "name") {

View File

@@ -107,11 +107,9 @@ export class HuiEntityCardEditor
private _computeLabelCallback = (schema: HaFormSchema) => {
if (schema.name === "entity") {
return `${this.hass!.localize(
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.required"
)})`;
);
}
return this.hass!.localize(

View File

@@ -1,25 +1,18 @@
import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-input/paper-input";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import "../../../../components/ha-form/ha-form";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { assert } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { domainIcon } from "../../../../common/entity/domain_icon";
import "../../../../components/ha-formfield";
import "../../../../components/ha-icon-picker";
import "../../../../components/ha-select";
import "../../../../components/ha-switch";
import { HomeAssistant } from "../../../../types";
import { EntitiesCardEntityConfig } from "../../cards/types";
import "../../components/hui-action-editor";
import "../../components/hui-entity-editor";
import "../../components/hui-theme-select-editor";
import { LovelaceRowEditor } from "../../types";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { EntitiesCardEntityConfig } from "../../cards/types";
import type { LovelaceRowEditor } from "../../types";
import { entitiesConfigStruct } from "../structs/entities-struct";
import { EditorTarget, EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
const SecondaryInfoValues: { [key: string]: { domains?: string[] } } = {
"entity-id": {},
@@ -45,128 +38,106 @@ export class HuiGenericEntityRowEditor
this._config = config;
}
get _entity(): string {
return this._config!.entity || "";
}
private _schema = memoizeOne(
(
entity: string,
icon: string | undefined,
entityState: HassEntity,
localize: LocalizeFunc
): HaFormSchema[] => {
const domain = computeDomain(entity);
get _name(): string {
return this._config!.name || "";
}
get _icon(): string {
return this._config!.icon || "";
}
get _secondary_info(): string {
return this._config!.secondary_info || "";
return [
{ name: "entity", required: true, selector: { entity: {} } },
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{
name: "icon",
selector: {
icon: {
placeholder: icon || entityState?.attributes.icon,
fallbackPath:
!icon && !entityState?.attributes.icon && entityState
? domainIcon(domain, entityState)
: undefined,
},
},
},
],
},
{
name: "secondary_info",
selector: {
select: {
options: Object.keys(SecondaryInfoValues)
.filter(
(info) =>
!("domains" in SecondaryInfoValues[info]) ||
("domains" in SecondaryInfoValues[info] &&
SecondaryInfoValues[info].domains!.includes(domain))
)
.map((info) => ({
value: info,
label: localize(
`ui.panel.lovelace.editor.card.entities.secondary_info_values.${info}`
),
})),
},
},
},
];
}
);
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
const domain = computeDomain(this._entity);
const entityState = this.hass.states[this._entity];
const entityState = this.hass.states[this._config.entity];
const schema = this._schema(
this._config.entity,
this._config.icon,
entityState,
this.hass.localize
);
return html`
<div class="card-config">
<ha-entity-picker
allow-custom-entity
<ha-form
.hass=${this.hass}
.value=${this._config.entity}
.configValue=${"entity"}
@change=${this._valueChanged}
></ha-entity-picker>
<div class="side-by-side">
<paper-input
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.name"
)}
.value=${this._config.name}
.configValue=${"name"}
.data=${this._config}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></paper-input>
<ha-icon-picker
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.icon"
)}
.value=${this._config.icon}
.configValue=${"icon"}
.placeholder=${entityState?.attributes.icon}
.fallbackPath=${!this._icon &&
!entityState?.attributes.icon &&
entityState
? domainIcon(computeDomain(entityState.entity_id), entityState)
: undefined}
@value-changed=${this._valueChanged}
></ha-icon-picker>
</div>
<ha-select
label="Secondary Info"
.configValue=${"secondary_info"}
@selected=${this._valueChanged}
@closed=${stopPropagation}
.value=${this._config.secondary_info || "none"}
naturalMenuWidth
fixedMenuPosition
>
<mwc-list-item value=""
>${this.hass!.localize(
"ui.panel.lovelace.editor.card.entities.secondary_info_values.none"
)}</mwc-list-item
>
${Object.keys(SecondaryInfoValues).map((info) => {
if (
!("domains" in SecondaryInfoValues[info]) ||
("domains" in SecondaryInfoValues[info] &&
SecondaryInfoValues[info].domains!.includes(domain))
) {
return html`
<mwc-list-item .value=${info}>
${this.hass!.localize(
`ui.panel.lovelace.editor.card.entities.secondary_info_values.${info}`
)}
</mwc-list-item>
`;
}
return "";
})}
</ha-select>
</div>
></ha-form>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
const value = target.value || ev.detail?.item?.value;
if (this[`_${target.configValue}`] === value) {
return;
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
if (target.configValue) {
if (value === "" || !value) {
this._config = { ...this._config };
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue!]: value,
private _computeLabelCallback = (schema: HaFormSchema) => {
if (schema.name === "entity") {
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.entity"
);
}
return (
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(
`ui.panel.lovelace.editor.card.entity-row.${schema.name}`
)
);
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
static get styles(): CSSResultGroup {
return configElementStyle;
}
}
declare global {
interface HTMLElementTagNameMap {

View File

@@ -1,16 +1,13 @@
import "@polymer/paper-input/paper-input";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import "../../../../components/ha-form/ha-form";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entity-picker";
import { HomeAssistant } from "../../../../types";
import { HumidifierCardConfig } from "../../cards/types";
import "../../components/hui-theme-select-editor";
import { LovelaceCardEditor } from "../../types";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { HumidifierCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditorTarget, EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -21,7 +18,21 @@ const cardConfigStruct = assign(
})
);
const includeDomains = ["humidifier"];
const SCHEMA: HaFormSchema[] = [
{
name: "entity",
required: true,
selector: { entity: { domain: "humidifer" } },
},
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
],
},
];
@customElement("hui-humidifier-card-editor")
export class HuiHumidifierCardEditor
@@ -37,81 +48,37 @@ export class HuiHumidifierCardEditor
this._config = config;
}
get _entity(): string {
return this._config!.entity || "";
}
get _name(): string {
return this._config!.name || "";
}
get _theme(): string {
return this._config!.theme || "";
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
return html`
<div class="card-config">
<ha-entity-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.required"
)})"
<ha-form
.hass=${this.hass}
.value=${this._entity}
.configValue=${"entity"}
.includeDomains=${includeDomains}
@change=${this._valueChanged}
allow-custom-entity
></ha-entity-picker>
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.name"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._name}
.configValue=${"name"}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></paper-input>
<hui-theme-select-editor
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
</div>
></ha-form>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (target.value === "") {
this._config = { ...this._config };
delete this._config[target.configValue!];
} else {
this._config = { ...this._config, [target.configValue!]: target.value };
}
}
fireEvent(this, "config-changed", { config: this._config });
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
static get styles(): CSSResultGroup {
return configElementStyle;
private _computeLabelCallback = (schema: HaFormSchema) => {
if (schema.name === "entity") {
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.entity"
);
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
};
}
declare global {

View File

@@ -1,14 +1,13 @@
import "@polymer/paper-input/paper-input";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import "../../../../components/ha-form/ha-form";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { HomeAssistant } from "../../../../types";
import { IframeCardConfig } from "../../cards/types";
import { LovelaceCardEditor } from "../../types";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { IframeCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditorTarget, EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -19,6 +18,18 @@ const cardConfigStruct = assign(
})
);
const SCHEMA: HaFormSchema[] = [
{ name: "title", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{ name: "url", required: true, selector: { text: {} } },
{ name: "aspect_ratio", selector: { text: {} } },
],
},
];
@customElement("hui-iframe-card-editor")
export class HuiIframeCardEditor
extends LitElement
@@ -33,85 +44,28 @@ export class HuiIframeCardEditor
this._config = config;
}
get _title(): string {
return this._config!.title || "";
}
get _url(): string {
return this._config!.url || "";
}
get _aspect_ratio(): string {
return this._config!.aspect_ratio || "";
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
return html`
<div class="card-config">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.url"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.required"
)})"
.value=${this._url}
.configValue=${"url"}
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></paper-input>
<div class="side-by-side">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.title"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._title}
.configValue=${"title"}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.aspect_ratio"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._aspect_ratio}
.configValue=${"aspect_ratio"}
@value-changed=${this._valueChanged}
></paper-input>
</div>
</div>
></ha-form>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
const value = target.value;
if (this[`_${target.configValue}`] === value) {
return;
}
if (target.configValue) {
if (value === "") {
this._config = { ...this._config };
delete this._config[target.configValue!];
} else {
this._config = { ...this._config, [target.configValue!]: value };
}
}
fireEvent(this, "config-changed", { config: this._config });
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
static get styles(): CSSResultGroup {
return configElementStyle;
}
private _computeLabelCallback = (schema: HaFormSchema) =>
this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`);
}
declare global {

View File

@@ -1,22 +1,21 @@
import "@polymer/paper-input/paper-input";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, object, optional, string, assign } from "superstruct";
import type { HassEntity } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-picker";
import { ActionConfig } from "../../../../data/lovelace";
import { HomeAssistant } from "../../../../types";
import { LightCardConfig } from "../../cards/types";
import type { ActionConfig } from "../../../../data/lovelace";
import type { HomeAssistant } from "../../../../types";
import type { LightCardConfig } from "../../cards/types";
import "../../components/hui-action-editor";
import "../../components/hui-entity-editor";
import "../../components/hui-theme-select-editor";
import { LovelaceCardEditor } from "../../types";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { EditorTarget } from "../types";
import type { EditorTarget } from "../types";
import { configElementStyle } from "./config-elements-style";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { domainIcon } from "../../../../common/entity/domain_icon";
import { computeDomain } from "../../../../common/entity/compute_domain";
import type { HaFormSchema } from "../../../../components/ha-form/types";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -30,8 +29,6 @@ const cardConfigStruct = assign(
})
);
const includeDomains = ["light"];
@customElement("hui-light-card-editor")
export class HuiLightCardEditor
extends LitElement
@@ -46,21 +43,39 @@ export class HuiLightCardEditor
this._config = config;
}
get _name(): string {
return this._config!.name || "";
}
get _theme(): string {
return this._config!.theme || "";
}
get _entity(): string {
return this._config!.entity || "";
}
get _icon(): string {
return this._config!.icon || "";
}
private _schema = memoizeOne(
(
entity: string,
icon: string | undefined,
entityState: HassEntity
): HaFormSchema[] => [
{
name: "entity",
required: true,
selector: { entity: { domain: "light" } },
},
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{
name: "icon",
selector: {
icon: {
placeholder: icon || entityState?.attributes.icon,
fallbackPath:
!icon && !entityState?.attributes.icon && entityState
? domainIcon(computeDomain(entity), entityState)
: undefined,
},
},
},
],
},
{ name: "theme", selector: { theme: {} } },
]
);
get _hold_action(): ActionConfig {
return this._config!.hold_action || { action: "more-info" };
@@ -84,59 +99,22 @@ export class HuiLightCardEditor
"none",
];
const entityState = this.hass.states[this._entity];
const entityState = this.hass.states[this._config.entity];
const schema = this._schema(
this._config.entity,
this._config.icon,
entityState
);
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
<div class="card-config">
<ha-entity-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.required"
)})"
.hass=${this.hass}
.value=${this._entity}
.configValue=${"entity"}
.includeDomains=${includeDomains}
@value-changed=${this._valueChanged}
allow-custom-entity
></ha-entity-picker>
<div class="side-by-side">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.name"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._name}
.configValue=${"name"}
@value-changed=${this._valueChanged}
></paper-input>
<ha-icon-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.icon"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._icon}
.placeholder=${this._icon || entityState?.attributes.icon}
.fallbackPath=${!this._icon &&
!entityState?.attributes.icon &&
entityState
? domainIcon(computeDomain(entityState.entity_id), entityState)
: undefined}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
></ha-icon-picker>
</div>
<hui-theme-select-editor
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
<hui-action-editor
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.hold_action"
@@ -147,7 +125,7 @@ export class HuiLightCardEditor
.config=${this._hold_action}
.actions=${actions}
.configValue=${"hold_action"}
@value-changed=${this._valueChanged}
@value-changed=${this._actionChanged}
></hui-action-editor>
<hui-action-editor
@@ -160,13 +138,13 @@ export class HuiLightCardEditor
.config=${this._double_tap_action}
.actions=${actions}
.configValue=${"double_tap_action"}
@value-changed=${this._valueChanged}
@value-changed=${this._actionChanged}
></hui-action-editor>
</div>
`;
}
private _valueChanged(ev: CustomEvent): void {
private _actionChanged(ev: CustomEvent): void {
if (!this._config || !this.hass) {
return;
}
@@ -190,9 +168,33 @@ export class HuiLightCardEditor
fireEvent(this, "config-changed", { config: this._config });
}
static get styles(): CSSResultGroup {
return configElementStyle;
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (schema: HaFormSchema) => {
if (schema.name === "entity") {
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.entity"
);
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
};
static styles: CSSResultGroup = [
configElementStyle,
css`
ha-form,
hui-action-editor {
display: block;
margin-bottom: 24px;
overflow: auto;
}
`,
];
}
declare global {

View File

@@ -1,5 +1,5 @@
import "@polymer/paper-input/paper-input";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import "../../../../components/ha-form/ha-form";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
array,
@@ -12,15 +12,11 @@ import {
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entities-picker";
import "../../../../components/entity/ha-entity-picker";
import { HomeAssistant } from "../../../../types";
import { LogbookCardConfig } from "../../cards/types";
import "../../components/hui-entity-editor";
import "../../components/hui-theme-select-editor";
import { LovelaceCardEditor } from "../../types";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { LogbookCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditorTarget } from "../types";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -32,6 +28,18 @@ const cardConfigStruct = assign(
})
);
const SCHEMA: HaFormSchema[] = [
{ name: "title", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{ name: "theme", selector: { theme: {} } },
{ name: "hours_to_show", selector: { number: { mode: "box", min: 1 } } },
],
},
];
@customElement("hui-logbook-card-editor")
export class HuiLogbookCardEditor
extends LitElement
@@ -41,67 +49,28 @@ export class HuiLogbookCardEditor
@state() private _config?: LogbookCardConfig;
@state() private _configEntities?: string[];
public setConfig(config: LogbookCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
this._configEntities = config.entities;
}
get _title(): string {
return this._config!.title || "";
}
get _entities(): string[] {
return this._config!.entities || [];
}
get _hours_to_show(): number {
return this._config!.hours_to_show || 24;
}
get _theme(): string {
return this._config!.theme || "";
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
return html`
<div class="card-config">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.title"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._title}
.configValue=${"title"}
@value-changed=${this._valueChanged}
></paper-input>
<div class="side-by-side">
<hui-theme-select-editor
<ha-form
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
<paper-input
type="number"
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.hours_to_show"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._hours_to_show}
min="1"
.configValue=${"hours_to_show"}
@value-changed=${this._valueChanged}
></paper-input>
</div>
></ha-form>
<h3>
${`${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.entities"
@@ -111,48 +80,27 @@ export class HuiLogbookCardEditor
</h3>
<ha-entities-picker
.hass=${this.hass}
.value=${this._configEntities}
@value-changed=${this._valueChanged}
.value=${this._entities}
@value-changed=${this._entitiesChanged}
>
</ha-entities-picker>
</div>
`;
}
private _valueChanged(ev: CustomEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (ev.detail && ev.detail.value && Array.isArray(ev.detail.value)) {
this._config = { ...this._config, entities: ev.detail.value };
} else if (target.configValue) {
if (target.value === "") {
this._config = { ...this._config };
delete this._config[target.configValue!];
} else {
let value: any = target.value;
if (target.type === "number") {
value = Number(value);
}
this._config = {
...this._config,
[target.configValue!]: value,
};
}
}
private _entitiesChanged(ev: CustomEvent): void {
this._config = { ...this._config!, entities: ev.detail.value };
fireEvent(this, "config-changed", { config: this._config });
}
static get styles(): CSSResultGroup {
return configElementStyle;
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (schema: HaFormSchema) =>
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(`ui.panel.lovelace.editor.card.logbook.${schema.name}`);
}
declare global {

View File

@@ -1,3 +1,4 @@
import "../../../../components/ha-form/ha-form";
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -12,7 +13,6 @@ import {
assign,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
import "../../../../components/ha-formfield";
import "../../../../components/ha-switch";
import { PolymerChangedEvent } from "../../../../polymer-types";
@@ -27,6 +27,7 @@ import { entitiesConfigStruct } from "../structs/entities-struct";
import { EditorTarget, EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { HaFormSchema } from "../../../../components/ha-form/types";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -41,6 +42,20 @@ const cardConfigStruct = assign(
})
);
const SCHEMA: HaFormSchema[] = [
{ name: "title", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{ name: "aspect_ratio", selector: { text: {} } },
{ name: "default_zoom", selector: { number: { mode: "box", min: 0 } } },
{ name: "dark_mode", selector: { boolean: {} } },
{ name: "hours_to_show", selector: { number: { mode: "box", min: 1 } } },
],
},
];
@customElement("hui-map-card-editor")
export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
@property({ attribute: false }) public hass?: HomeAssistant;
@@ -57,99 +72,24 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
: [];
}
get _title(): string {
return this._config!.title || "";
}
get _aspect_ratio(): string {
return this._config!.aspect_ratio || "";
}
get _default_zoom(): number {
return this._config!.default_zoom || 0;
}
get _geo_location_sources(): string[] {
return this._config!.geo_location_sources || [];
}
get _hours_to_show(): number {
return this._config!.hours_to_show || 0;
}
get _dark_mode(): boolean {
return this._config!.dark_mode || false;
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
<div class="card-config">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.title"
)}
(${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._title}
.configValue=${"title"}
@value-changed=${this._valueChanged}
></paper-input>
<div class="side-by-side">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.aspect_ratio"
)}
(${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._aspect_ratio}
.configValue=${"aspect_ratio"}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.map.default_zoom"
)}
(${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
type="number"
.value=${this._default_zoom}
.configValue=${"default_zoom"}
@value-changed=${this._valueChanged}
></paper-input>
</div>
<div class="side-by-side">
<ha-formfield
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.map.dark_mode"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.checked=${this._dark_mode}
.configValue=${"dark_mode"}
@change=${this._valueChanged}
></ha-switch
></ha-formfield>
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.map.hours_to_show"
)}
(${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
type="number"
.value=${this._hours_to_show}
.configValue=${"hours_to_show"}
@value-changed=${this._valueChanged}
></paper-input>
</div>
<hui-entity-editor
.hass=${this.hass}
.entities=${this._configEntities}
@@ -167,8 +107,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
)}
.hass=${this.hass}
.value=${this._geo_location_sources}
.configValue=${"geo_location_sources"}
@value-changed=${this._valueChanged}
@value-changed=${this._geoSourcesChanged}
></hui-input-list-editor>
</div>
</div>
@@ -176,18 +115,15 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
}
private _entitiesValueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
if (ev.detail && ev.detail.entities) {
this._config = { ...this._config, entities: ev.detail.entities };
this._config = { ...this._config!, entities: ev.detail.entities };
this._configEntities = processEditorEntities(this._config.entities);
fireEvent(this, "config-changed", { config: this._config });
fireEvent(this, "config-changed", { config: this._config! });
}
}
private _valueChanged(ev: PolymerChangedEvent<any>): void {
private _geoSourcesChanged(ev: PolymerChangedEvent<any>): void {
if (!this._config || !this.hass) {
return;
}
@@ -196,26 +132,34 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
return;
}
let value = target.checked ?? ev.detail.value;
const value = ev.detail.value;
if (value && target.type === "number") {
value = Number(value);
}
if (this[`_${target.configValue}`] === value) {
if (this._geo_location_sources === value) {
return;
}
if (value === "") {
this._config = { ...this._config };
delete this._config[target.configValue!];
} else if (target.configValue) {
delete this._config.geo_location_sources;
} else {
this._config = {
...this._config,
[target.configValue]: value,
geo_location_sources: value,
};
}
fireEvent(this, "config-changed", { config: this._config });
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (schema: HaFormSchema) =>
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(`ui.panel.lovelace.editor.card.map.${schema.name}`);
static get styles(): CSSResultGroup {
return [
configElementStyle,

View File

@@ -1,16 +1,13 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import "../../../../components/ha-form/ha-form";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { HomeAssistant } from "../../../../types";
import { MarkdownCardConfig } from "../../cards/types";
import "../../components/hui-theme-select-editor";
import { LovelaceCardEditor } from "../../types";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { MarkdownCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditorTarget, EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -21,6 +18,12 @@ const cardConfigStruct = assign(
})
);
const SCHEMA: HaFormSchema[] = [
{ name: "title", selector: { text: {} } },
{ name: "content", required: true, selector: { text: { multiline: true } } },
{ name: "theme", selector: { theme: {} } },
];
@customElement("hui-markdown-card-editor")
export class HuiMarkdownCardEditor
extends LitElement
@@ -35,90 +38,33 @@ export class HuiMarkdownCardEditor
this._config = config;
}
get _title(): string {
return this._config!.title || "";
}
get _content(): string {
return this._config!.content || "";
}
get _theme(): string {
return this._config!.theme || "";
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
return html`
<div class="card-config">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.title"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._title}
.configValue=${"title"}
@value-changed=${this._valueChanged}
></paper-input>
<paper-textarea
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.markdown.content"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.required"
)})"
.value=${this._content}
.configValue=${"content"}
@keydown=${this._ignoreKeydown}
@value-changed=${this._valueChanged}
autocapitalize="none"
autocomplete="off"
spellcheck="false"
></paper-textarea>
<hui-theme-select-editor
<ha-form
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
</div>
></ha-form>
`;
}
private _ignoreKeydown(ev: KeyboardEvent) {
// Stop keyboard events from the paper-textarea from propagating to avoid accidentally closing the dialog when the user presses Enter.
ev.stopPropagation();
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (target.value === "" && target.configValue !== "content") {
this._config = { ...this._config };
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue!]: target.value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
static get styles(): CSSResultGroup {
return configElementStyle;
}
private _computeLabelCallback = (schema: HaFormSchema) =>
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(
`ui.panel.lovelace.editor.card.markdown.${schema.name}`
);
}
declare global {

View File

@@ -1,5 +1,5 @@
import "@polymer/paper-input/paper-input";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, object, optional, string, assign } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
@@ -63,7 +63,7 @@ export class HuiPictureCardEditor
return html`
<div class="card-config">
<paper-input
<ha-textfield
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.image"
)} (${this.hass.localize(
@@ -71,8 +71,8 @@ export class HuiPictureCardEditor
)})"
.value=${this._image}
.configValue=${"image"}
@value-changed=${this._valueChanged}
></paper-input>
@input=${this._valueChanged}
></ha-textfield>
<hui-theme-select-editor
.hass=${this.hass}
.value=${this._theme}
@@ -134,7 +134,15 @@ export class HuiPictureCardEditor
}
static get styles(): CSSResultGroup {
return configElementStyle;
return [
configElementStyle,
css`
ha-textfield {
display: block;
margin-bottom: 8px;
}
`,
];
}
}

View File

@@ -1,24 +1,16 @@
import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-input/paper-input";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
import "../../../../components/ha-formfield";
import "../../../../components/ha-select";
import "../../../../components/ha-switch";
import { ActionConfig } from "../../../../data/lovelace";
import { HomeAssistant } from "../../../../types";
import { PictureEntityCardConfig } from "../../cards/types";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type { ActionConfig } from "../../../../data/lovelace";
import type { HomeAssistant } from "../../../../types";
import type { PictureEntityCardConfig } from "../../cards/types";
import "../../components/hui-action-editor";
import "../../components/hui-entity-editor";
import "../../components/hui-theme-select-editor";
import { LovelaceCardEditor } from "../../types";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditorTarget } from "../types";
import type { EditorTarget } from "../types";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign(
@@ -38,7 +30,38 @@ const cardConfigStruct = assign(
})
);
const includeDomains = ["camera"];
const SCHEMA: HaFormSchema[] = [
{ name: "entity", required: true, selector: { entity: {} } },
{ name: "name", selector: { text: {} } },
{ name: "image", selector: { text: {} } },
{ name: "camera_image", selector: { entity: { domain: "camera" } } },
{
name: "",
type: "grid",
schema: [
{
name: "camera_view",
selector: { select: { options: ["auto", "live"] } },
},
{ name: "aspect_ratio", selector: { text: {} } },
],
},
{
name: "",
type: "grid",
schema: [
{
name: "show_name",
selector: { boolean: {} },
},
{
name: "show_state",
selector: { boolean: {} },
},
],
},
{ name: "theme", selector: { theme: {} } },
];
@customElement("hui-picture-entity-card-editor")
export class HuiPictureEntityCardEditor
@@ -54,30 +77,6 @@ export class HuiPictureEntityCardEditor
this._config = config;
}
get _entity(): string {
return this._config!.entity || "";
}
get _name(): string {
return this._config!.name || "";
}
get _image(): string {
return this._config!.image || "";
}
get _camera_image(): string {
return this._config!.camera_image || "";
}
get _camera_view(): string {
return this._config!.camera_view || "auto";
}
get _aspect_ratio(): string {
return this._config!.aspect_ratio || "";
}
get _tap_action(): ActionConfig {
return this._config!.tap_action || { action: "more-info" };
}
@@ -86,140 +85,29 @@ export class HuiPictureEntityCardEditor
return this._config!.hold_action;
}
get _show_name(): boolean {
return this._config!.show_name ?? true;
}
get _show_state(): boolean {
return this._config!.show_state ?? true;
}
get _theme(): string {
return this._config!.theme || "";
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
const actions = ["more-info", "toggle", "navigate", "call-service", "none"];
const views = ["auto", "live"];
const dir = computeRTLDirection(this.hass!);
const data = {
show_state: true,
show_name: true,
camera_view: "auto",
...this._config,
};
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
<div class="card-config">
<ha-entity-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.required"
)})"
.hass=${this.hass}
.value=${this._entity}
.configValue=${"entity"}
@value-changed=${this._valueChanged}
allow-custom-entity
></ha-entity-picker>
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.name"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._name}
.configValue=${"name"}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.image"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._image}
.configValue=${"image"}
@value-changed=${this._valueChanged}
></paper-input>
<ha-entity-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.camera_image"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.hass=${this.hass}
.value=${this._camera_image}
.configValue=${"camera_image"}
@value-changed=${this._valueChanged}
.includeDomains=${includeDomains}
allow-custom-entity
></ha-entity-picker>
<div class="side-by-side">
<ha-select
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.camera_view"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.configValue=${"camera_view"}
@selected=${this._valueChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${views.indexOf(this._camera_view)}
>
${views.map(
(view) =>
html`<mwc-list-item .value=${view}>${view}</mwc-list-item> `
)}
</ha-select>
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.aspect_ratio"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._aspect_ratio}
.configValue=${"aspect_ratio"}
@value-changed=${this._valueChanged}
></paper-input>
</div>
<div class="side-by-side">
<div>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.show_name"
)}
.dir=${dir}
>
<ha-switch
.checked=${this._config!.show_name !== false}
.configValue=${"show_name"}
@change=${this._change}
></ha-switch
></ha-formfield>
</div>
<div>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.show_state"
)}
.dir=${dir}
>
<ha-switch
.checked=${this._config!.show_state !== false}
.configValue=${"show_state"}
@change=${this._change}
></ha-switch
></ha-formfield>
</div>
</div>
<hui-theme-select-editor
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
<div class="side-by-side">
<hui-action-editor
.label="${this.hass.localize(
@@ -231,7 +119,7 @@ export class HuiPictureEntityCardEditor
.config=${this._tap_action}
.actions=${actions}
.configValue=${"tap_action"}
@value-changed=${this._valueChanged}
@value-changed=${this._changed}
></hui-action-editor>
<hui-action-editor
.label="${this.hass.localize(
@@ -243,32 +131,18 @@ export class HuiPictureEntityCardEditor
.config=${this._hold_action}
.actions=${actions}
.configValue=${"hold_action"}
@value-changed=${this._valueChanged}
@value-changed=${this._changed}
></hui-action-editor>
</div>
</div>
`;
}
private _change(ev: Event) {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
const value = target.checked;
if (this[`_${target.configValue}`] === value) {
return;
}
this._config = {
...this._config,
[target.configValue!]: value,
};
fireEvent(this, "config-changed", { config: this._config });
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _changed(ev: CustomEvent): void {
if (!this._config || !this.hass) {
return;
}
@@ -279,7 +153,6 @@ export class HuiPictureEntityCardEditor
return;
}
if (target.configValue) {
if (value !== false && !value) {
this._config = { ...this._config };
delete this._config[target.configValue!];
@@ -289,13 +162,27 @@ export class HuiPictureEntityCardEditor
[target.configValue!]: value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
}
static get styles(): CSSResultGroup {
return configElementStyle;
private _computeLabelCallback = (schema: HaFormSchema) => {
if (schema.name === "entity") {
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.entity"
);
}
return (
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(
`ui.panel.lovelace.editor.card.picture-entity.${schema.name}`
)
);
};
static styles: CSSResultGroup = configElementStyle;
}
declare global {

View File

@@ -1,25 +1,21 @@
import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-input/paper-input";
import "../../components/hui-action-editor";
import "../../../../components/ha-form/ha-form";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { array, assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-select";
import { ActionConfig } from "../../../../data/lovelace";
import { HomeAssistant } from "../../../../types";
import { PictureGlanceCardConfig } from "../../cards/types";
import "../../components/hui-action-editor";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type { ActionConfig } from "../../../../data/lovelace";
import type { HomeAssistant } from "../../../../types";
import type { PictureGlanceCardConfig } from "../../cards/types";
import "../../components/hui-entity-editor";
import "../../components/hui-theme-select-editor";
import { EntityConfig } from "../../entity-rows/types";
import { LovelaceCardEditor } from "../../types";
import type { EntityConfig } from "../../entity-rows/types";
import type { LovelaceCardEditor } from "../../types";
import { processEditorEntities } from "../process-editor-entities";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entitiesConfigStruct } from "../structs/entities-struct";
import { EditorTarget } from "../types";
import type { EditorTarget } from "../types";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign(
@@ -38,7 +34,26 @@ const cardConfigStruct = assign(
})
);
const includeDomains = ["camera"];
const actions = ["more-info", "toggle", "navigate", "call-service", "none"];
const SCHEMA: HaFormSchema[] = [
{ name: "title", selector: { text: {} } },
{ name: "image", selector: { text: {} } },
{ name: "camera_image", selector: { entity: { domain: "camera" } } },
{
name: "",
type: "grid",
schema: [
{
name: "camera_view",
selector: { select: { options: ["auto", "live"] } },
},
{ name: "aspect_ratio", selector: { text: {} } },
],
},
{ name: "entity", selector: { entity: {} } },
{ name: "theme", selector: { theme: {} } },
];
@customElement("hui-picture-glance-card-editor")
export class HuiPictureGlanceCardEditor
@@ -57,39 +72,6 @@ export class HuiPictureGlanceCardEditor
this._configEntities = processEditorEntities(config.entities);
}
get _entity(): string {
return this._config!.entity || "";
}
get _title(): string {
return this._config!.title || "";
}
get _image(): string {
return (
this._config!.image ||
(this._camera_image
? ""
: "https://www.home-assistant.io/images/merchandise/shirt-frontpage.png")
);
}
get _camera_image(): string {
return this._config!.camera_image || "";
}
get _camera_view(): string {
return this._config!.camera_view || "auto";
}
get _state_image(): Record<string, unknown> {
return this._config!.state_image || {};
}
get _aspect_ratio(): string {
return this._config!.aspect_ratio || "";
}
get _tap_action(): ActionConfig {
return this._config!.tap_action || { action: "toggle" };
}
@@ -98,102 +80,27 @@ export class HuiPictureGlanceCardEditor
return this._config!.hold_action || { action: "more-info" };
}
get _theme(): string {
return this._config!.theme || "";
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
const actions = ["more-info", "toggle", "navigate", "call-service", "none"];
const views = ["auto", "live"];
const data = { camera_view: "auto", ...this._config };
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
<div class="card-config">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.title"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._title}
.configValue=${"title"}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.image"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._image}
.configValue=${"image"}
@value-changed=${this._valueChanged}
></paper-input>
<ha-entity-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.camera_image"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.hass=${this.hass}
.value=${this._camera_image}
.configValue=${"camera_image"}
@value-changed=${this._valueChanged}
allow-custom-entity
.includeDomains=${includeDomains}
></ha-entity-picker>
<div class="side-by-side">
<ha-select
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.camera_view"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.configValue=${"camera_view"}
@selected=${this._valueChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._camera_view}
>
${views.map(
(view) =>
html`<mwc-list-item .value=${view}>${view}</mwc-list-item> `
)}
</ha-select>
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.aspect_ratio"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._aspect_ratio}
.configValue=${"aspect_ratio"}
@value-changed=${this._valueChanged}
></paper-input>
</div>
<ha-entity-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.picture-glance.state_entity"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.hass=${this.hass}
.value=${this._entity}
.configValue=${"entity"}
@value-changed=${this._valueChanged}
allow-custom-entity
></ha-entity-picker>
<div class="side-by-side">
<hui-action-editor
.label="${this.hass.localize(
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.tap_action"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
)}
.hass=${this.hass}
.config=${this._tap_action}
.actions=${actions}
@@ -201,11 +108,9 @@ export class HuiPictureGlanceCardEditor
@value-changed=${this._valueChanged}
></hui-action-editor>
<hui-action-editor
.label="${this.hass.localize(
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.hold_action"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
)}
.hass=${this.hass}
.config=${this._hold_action}
.actions=${actions}
@@ -216,19 +121,17 @@ export class HuiPictureGlanceCardEditor
<hui-entity-editor
.hass=${this.hass}
.entities=${this._configEntities}
@entities-changed=${this._valueChanged}
@entities-changed=${this._changed}
></hui-entity-editor>
<hui-theme-select-editor
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
</div>
`;
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _changed(ev: CustomEvent): void {
if (!this._config || !this.hass) {
return;
}
@@ -257,9 +160,24 @@ export class HuiPictureGlanceCardEditor
fireEvent(this, "config-changed", { config: this._config });
}
static get styles(): CSSResultGroup {
return configElementStyle;
private _computeLabelCallback = (schema: HaFormSchema) => {
if (schema.name === "entity") {
return this.hass!.localize(
"ui.panel.lovelace.editor.card.picture-glance.state_entity"
);
}
return (
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(
`ui.panel.lovelace.editor.card.picture-glance.${schema.name}`
)
);
};
static styles: CSSResultGroup = configElementStyle;
}
declare global {

View File

@@ -1,16 +1,13 @@
import "@polymer/paper-input/paper-input";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import "../../../../components/ha-form/ha-form";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entity-picker";
import { HomeAssistant } from "../../../../types";
import { PlantStatusCardConfig } from "../../cards/types";
import "../../components/hui-theme-select-editor";
import { LovelaceCardEditor } from "../../types";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { PlantStatusCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditorTarget, EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -21,7 +18,11 @@ const cardConfigStruct = assign(
})
);
const includeDomains = ["plant"];
const SCHEMA: HaFormSchema[] = [
{ name: "entity", required: true, selector: { entity: { domain: "plant" } } },
{ name: "name", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
];
@customElement("hui-plant-status-card-editor")
export class HuiPlantStatusCardEditor
@@ -37,83 +38,37 @@ export class HuiPlantStatusCardEditor
this._config = config;
}
get _entity(): string {
return this._config!.entity || "";
}
get _name(): string {
return this._config!.name || "";
}
get _theme(): string {
return this._config!.theme || "";
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
return html`
<div class="card-config">
<ha-entity-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.required"
)})"
<ha-form
.hass=${this.hass}
.value=${this._entity}
.configValue=${"entity"}
.includeDomains=${includeDomains}
@change=${this._valueChanged}
allow-custom-entity
></ha-entity-picker>
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.name"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._name}
.configValue=${"name"}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></paper-input>
<hui-theme-select-editor
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
</div>
></ha-form>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (target.value === "") {
this._config = { ...this._config };
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue!]: target.value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
static get styles(): CSSResultGroup {
return configElementStyle;
private _computeLabelCallback = (schema: HaFormSchema) => {
if (schema.name === "entity") {
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.entity"
);
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
};
}
declare global {

View File

@@ -1,7 +1,8 @@
import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-input/paper-input";
import "../../../../components/ha-form/ha-form";
import type { HassEntity } from "home-assistant-js-websocket";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
assert,
assign,
@@ -13,20 +14,13 @@ import {
union,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { domainIcon } from "../../../../common/entity/domain_icon";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-formfield";
import "../../../../components/ha-icon-picker";
import "../../../../components/ha-select";
import "../../../../components/ha-switch";
import { HomeAssistant } from "../../../../types";
import { SensorCardConfig } from "../../cards/types";
import "../../components/hui-theme-select-editor";
import { LovelaceCardEditor } from "../../types";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { SensorCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditorTarget, EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign(
@@ -43,8 +37,6 @@ const cardConfigStruct = assign(
})
);
const includeDomains = ["counter", "input_number", "number", "sensor"];
@customElement("hui-sensor-card-editor")
export class HuiSensorCardEditor
extends LitElement
@@ -59,201 +51,122 @@ export class HuiSensorCardEditor
this._config = config;
}
get _entity(): string {
return this._config!.entity || "";
}
get _name(): string {
return this._config!.name || "";
}
get _icon(): string {
return this._config!.icon || "";
}
get _graph(): string {
return this._config!.graph || "none";
}
get _unit(): string {
return this._config!.unit || "";
}
get _detail(): number {
return this._config!.detail ?? 1;
}
get _theme(): string {
return this._config!.theme || "";
}
get _hours_to_show(): number | string {
return this._config!.hours_to_show || "24";
}
private _schema = memoizeOne(
(
entity: string,
icon: string | undefined,
entityState: HassEntity
): HaFormSchema[] => [
{
name: "entity",
selector: {
entity: { domain: ["counter", "input_number", "number", "sensor"] },
},
},
{ name: "name", selector: { text: {} } },
{
type: "grid",
name: "",
schema: [
{
name: "icon",
selector: {
icon: {
placeholder: icon || entityState?.attributes.icon,
fallbackPath:
!icon && !entityState?.attributes.icon && entityState
? domainIcon(computeDomain(entity), entityState)
: undefined,
},
},
},
{
name: "graph",
selector: {
select: {
options: [
{
value: "none",
label: "None",
},
{
value: "line",
label: "Line",
},
],
},
},
},
{ name: "unit", selector: { text: {} } },
{ name: "detail", selector: { boolean: {} } },
{ name: "theme", selector: { theme: {} } },
{
name: "hours_to_show",
selector: { number: { min: 1, mode: "box" } },
},
],
},
]
);
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
const graphs = ["line", "none"];
const entityState = this.hass.states[this._config.entity];
const entityState = this.hass.states[this._entity];
const schema = this._schema(
this._config.entity,
this._config.icon,
entityState
);
const data = {
hours_to_show: 24,
graph: "none",
...this._config,
detail: this._config!.detail === 2,
};
return html`
<div class="card-config">
<ha-entity-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.required"
)})"
<ha-form
.hass=${this.hass}
.value=${this._entity}
.configValue=${"entity"}
.includeDomains=${includeDomains}
@change=${this._valueChanged}
allow-custom-entity
></ha-entity-picker>
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.name"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._name}
.configValue=${"name"}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></paper-input>
<div class="side-by-side">
<ha-icon-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.icon"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._icon}
.placeholder=${this._icon || entityState?.attributes.icon}
.fallbackPath=${!this._icon &&
!entityState?.attributes.icon &&
entityState
? domainIcon(computeDomain(entityState.entity_id), entityState)
: undefined}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
></ha-icon-picker>
<ha-select
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.sensor.graph_type"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.configValue=${"graph"}
@selected=${this._valueChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._graph}
>
${graphs.map(
(graph) =>
html`<mwc-list-item .value=${graph}>${graph}</mwc-list-item>`
)}
</ha-select>
</div>
<div class="side-by-side">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.unit"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._unit}
.configValue=${"unit"}
@value-changed=${this._valueChanged}
></paper-input>
<ha-formfield
label=${this.hass.localize(
"ui.panel.lovelace.editor.card.sensor.show_more_detail"
)}
>
<ha-switch
.checked=${this._detail === 2}
.configValue=${"detail"}
@change=${this._change}
></ha-switch>
</ha-formfield>
</div>
<div class="side-by-side">
<hui-theme-select-editor
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.hours_to_show"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
type="number"
.value=${this._hours_to_show}
min="1"
.configValue=${"hours_to_show"}
@value-changed=${this._valueChanged}
></paper-input>
</div>
</div>
></ha-form>
`;
}
private _change(ev: Event) {
if (!this._config || !this.hass) {
return;
private _valueChanged(ev: CustomEvent): void {
const config = ev.detail.value;
config.detail = config.detail ? 2 : 1;
fireEvent(this, "config-changed", { config });
}
const value = (ev.target! as EditorTarget).checked ? 2 : 1;
if (this._detail === value) {
return;
private _computeLabelCallback = (schema: HaFormSchema) => {
if (schema.name === "entity") {
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.entity"
);
}
this._config = {
...this._config,
detail: value,
if (schema.name === "detail") {
return this.hass!.localize(
"ui.panel.lovelace.editor.card.sensor.show_more_detail"
);
}
return (
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(`ui.panel.lovelace.editor.card.sensor.${schema.name}`)
);
};
fireEvent(this, "config-changed", { config: this._config });
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (
target.value === "" ||
(target.type === "number" && isNaN(Number(target.value)))
) {
this._config = { ...this._config };
delete this._config[target.configValue!];
} else {
let value: any = target.value;
if (target.type === "number") {
value = Number(value);
}
this._config = { ...this._config, [target.configValue!]: value };
}
}
fireEvent(this, "config-changed", { config: this._config });
}
static get styles(): CSSResultGroup {
return configElementStyle;
}

View File

@@ -1,4 +1,4 @@
import "@polymer/paper-input/paper-input";
import "../../../../components/ha-textfield";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
@@ -57,7 +57,7 @@ export class HuiShoppingListEditor
</div>
`
: ""}
<paper-input
<ha-textfield
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.title"
)} (${this.hass.localize(
@@ -65,8 +65,8 @@ export class HuiShoppingListEditor
)})"
.value=${this._title}
.configValue=${"title"}
@value-changed=${this._valueChanged}
></paper-input>
@input=${this._valueChanged}
></ha-textfield>
<hui-theme-select-editor
.hass=${this.hass}
.value=${this._theme}

View File

@@ -1,7 +1,7 @@
import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-input/paper-input";
import "../../../../components/ha-form/ha-form";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
array,
assert,
@@ -14,22 +14,15 @@ import {
union,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/entity/ha-statistics-picker";
import "../../../../components/ha-checkbox";
import "../../../../components/ha-formfield";
import "../../../../components/ha-radio";
import type { HaRadio } from "../../../../components/ha-radio";
import "../../../../components/ha-select";
import { StatisticType } from "../../../../data/history";
import { HomeAssistant } from "../../../../types";
import { StatisticsGraphCardConfig } from "../../cards/types";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { StatisticsGraphCardConfig } from "../../cards/types";
import { processConfigEntities } from "../../common/process-config-entities";
import { LovelaceCardEditor } from "../../types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entitiesConfigStruct } from "../structs/entities-struct";
import { EditorTarget } from "../types";
import { configElementStyle } from "./config-elements-style";
const statTypeStruct = union([
literal("sum"),
@@ -78,133 +71,82 @@ export class HuiStatisticsGraphCardEditor
: [];
}
get _title(): string {
return this._config!.title || "";
}
get _days_to_show(): number {
return this._config!.days_to_show || 30;
}
get _period(): string {
return this._config!.period || "hour";
}
get _chart_type(): StatisticsGraphCardConfig["chart_type"] {
return this._config!.chart_type || "line";
}
get _stat_types(): StatisticType[] {
return this._config!.stat_types
? Array.isArray(this._config!.stat_types)
? this._config!.stat_types
: [this._config!.stat_types]
: ["mean", "min", "max", "sum"];
}
private _schema = memoizeOne((localize: LocalizeFunc) => [
{ name: "title", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{
name: "period",
required: true,
selector: {
select: {
options: periods.map((period) => ({
value: period,
label: localize(
`ui.panel.lovelace.editor.card.statistics-graph.periods.${period}`
),
})),
},
},
},
{
name: "days_to_show",
required: true,
selector: { number: { min: 1, mode: "box" } },
},
{
name: "stat_types",
required: true,
type: "multi_select",
options: [
["mean", "Mean"],
["min", "Min"],
["max", "Max"],
["sum", "Sum"],
],
},
{
name: "chart_type",
required: true,
type: "select",
options: [
["line", "Line"],
["bar", "Bar"],
],
},
],
},
]);
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
const schema = this._schema(this.hass.localize);
const stat_types = this._config!.stat_types
? Array.isArray(this._config!.stat_types)
? this._config!.stat_types
: [this._config!.stat_types]
: ["mean", "min", "max", "sum"];
const data = {
chart_type: "line",
period: "hour",
days_to_show: 30,
...this._config,
stat_types,
};
return html`
<div class="card-config">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.title"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._title}
.configValue=${"title"}
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></paper-input>
<div class="side-by-side">
<ha-select
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.statistics-graph.period"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.configValue=${"period"}
@selected=${this._periodSelected}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._period}
>
${periods.map(
(period) =>
html`<mwc-list-item .value=${period}>
${this.hass!.localize(
`ui.panel.lovelace.editor.card.statistics-graph.periods.${period}`
)}
</mwc-list-item>`
)}
</ha-select>
<paper-input
type="number"
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.days_to_show"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._days_to_show}
min="1"
.configValue=${"days_to_show"}
@value-changed=${this._valueChanged}
></paper-input>
</div>
<p>Show stat types:</p>
<div class="side-by-side">
<ha-formfield label="Sum">
<ha-checkbox
.checked=${this._stat_types.includes("sum")}
name="sum"
@change=${this._statTypesChanged}
></ha-checkbox>
</ha-formfield>
<ha-formfield label="Mean">
<ha-checkbox
.checked=${this._stat_types.includes("mean")}
name="mean"
@change=${this._statTypesChanged}
></ha-checkbox>
</ha-formfield>
<ha-formfield label="Min">
<ha-checkbox
.checked=${this._stat_types.includes("min")}
name="min"
@change=${this._statTypesChanged}
></ha-checkbox>
</ha-formfield>
<ha-formfield label="Max">
<ha-checkbox
.checked=${this._stat_types.includes("max")}
name="max"
@change=${this._statTypesChanged}
></ha-checkbox>
</ha-formfield>
</div>
<div class="side-by-side">
<p>Chart type:</p>
<ha-formfield label="Line">
<ha-radio
.checked=${this._chart_type === "line"}
value="line"
name="chart_type"
@change=${this._chartTypeChanged}
></ha-radio>
</ha-formfield>
<ha-formfield label="Bar">
<ha-radio
.checked=${this._chart_type === "bar"}
value="bar"
name="chart_type"
@change=${this._chartTypeChanged}
></ha-radio>
</ha-formfield>
</div>
></ha-form>
<ha-statistics-picker
.hass=${this.hass}
.pickStatisticLabel=${`Add a statistic`}
@@ -217,82 +159,23 @@ export class HuiStatisticsGraphCardEditor
`;
}
private _chartTypeChanged(ev: CustomEvent) {
const input = ev.currentTarget as HaRadio;
fireEvent(this, "config-changed", {
config: { ...this._config!, chart_type: input.value },
});
}
private _statTypesChanged(ev) {
const name = ev.currentTarget.name;
const checked = ev.currentTarget.checked;
if (checked) {
fireEvent(this, "config-changed", {
config: { ...this._config!, stat_types: [...this._stat_types, name] },
});
return;
}
const statTypes = [...this._stat_types];
fireEvent(this, "config-changed", {
config: {
...this._config!,
stat_types: statTypes.filter((stat) => stat !== name),
},
});
}
private _periodSelected(ev) {
const newPeriod = ev.target.value
.period as StatisticsGraphCardConfig["period"];
if (newPeriod === this._period) {
return;
}
fireEvent(this, "config-changed", {
config: { ...this._config!, period: newPeriod },
});
}
private _valueChanged(ev: CustomEvent): void {
if (!this._config || !this.hass) {
return;
fireEvent(this, "config-changed", { config: ev.detail.value });
}
const target = ev.target! as EditorTarget;
private _computeLabelCallback = (schema: HaFormSchema) =>
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(
`ui.panel.lovelace.editor.card.statistics-graph.${schema.name}`
);
const newValue = ev.detail?.value || target.value;
if (this[`_${target.configValue}`] === newValue) {
return;
}
if (newValue === "") {
this._config = { ...this._config };
delete this._config[target.configValue!];
} else {
let value: any = newValue;
if (target.type === "number") {
value = Number(value);
}
this._config = {
...this._config,
[target.configValue!]: value,
};
}
fireEvent(this, "config-changed", { config: this._config });
}
static get styles(): CSSResultGroup {
return [
configElementStyle,
css`
static styles: CSSResultGroup = css`
ha-statistics-picker {
width: 100%;
}
`,
];
}
`;
}
declare global {

View File

@@ -66,11 +66,9 @@ export class HuiThermostatCardEditor
private _computeLabelCallback = (schema: HaFormSchema) => {
if (schema.name === "entity") {
return `${this.hass!.localize(
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.required"
)})`;
);
}
return this.hass!.localize(

View File

@@ -1,22 +1,18 @@
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import "../../../../components/ha-form/ha-form";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, boolean, object, optional, string, assign } from "superstruct";
import { memoize } from "@fullcalendar/common";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
import "../../../../components/entity/ha-entity-attribute-picker";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-formfield";
import "../../../../components/ha-radio";
import { HomeAssistant } from "../../../../types";
import { WeatherForecastCardConfig } from "../../cards/types";
import "../../components/hui-theme-select-editor";
import { LovelaceCardEditor } from "../../types";
import type { HomeAssistant } from "../../../../types";
import type { WeatherForecastCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { EditorTarget, EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { UNAVAILABLE } from "../../../../data/entity";
import { WeatherEntity } from "../../../../data/weather";
import type { WeatherEntity } from "../../../../data/weather";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { HaFormSchema } from "../../../../components/ha-form/types";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -33,8 +29,6 @@ const cardConfigStruct = assign(
})
);
const includeDomains = ["weather"];
@customElement("hui-weather-forecast-card-editor")
export class HuiWeatherForecastCardEditor
extends LitElement
@@ -61,30 +55,6 @@ export class HuiWeatherForecastCardEditor
}
}
get _entity(): string {
return this._config!.entity || "";
}
get _name(): string {
return this._config!.name || "";
}
get _theme(): string {
return this._config!.theme || "";
}
get _show_current(): boolean {
return this._config!.show_current ?? true;
}
get _show_forecast(): boolean {
return this._config!.show_forecast ?? this._has_forecast === true;
}
get _secondary_info_attribute(): string {
return this._config!.secondary_info_attribute || "";
}
get _has_forecast(): boolean | undefined {
if (this.hass && this._config) {
const stateObj = this.hass.states[this._config.entity] as WeatherEntity;
@@ -95,146 +65,134 @@ export class HuiWeatherForecastCardEditor
return undefined;
}
private _schema = memoize(
(
entity: string,
localize: LocalizeFunc,
hasForecast?: boolean
): HaFormSchema[] => {
const schema: HaFormSchema[] = [
{
name: "entity",
required: true,
selector: { entity: { domain: "weather" } },
},
{ name: "name", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{
name: "secondary_info_attribute",
selector: { attribute: { entity_id: entity } },
},
{ name: "theme", selector: { theme: {} } },
],
},
];
if (hasForecast) {
schema.push({
name: "forecast",
selector: {
select: {
options: [
{
value: "show_both",
label: localize(
"ui.panel.lovelace.editor.card.weather-forecast.show_both"
),
},
{
value: "show_current",
label: localize(
"ui.panel.lovelace.editor.card.weather-forecast.show_only_current"
),
},
{
value: "show_forecast",
label: localize(
"ui.panel.lovelace.editor.card.weather-forecast.show_only_forecast"
),
},
],
},
},
});
}
return schema;
}
);
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
const schema = this._schema(
this._config.entity,
this.hass.localize,
this._has_forecast
);
const data: WeatherForecastCardConfig = {
show_current: true,
show_forecast: this._has_forecast,
...this._config,
};
data.forecast =
data.show_current && data.show_forecast
? "show_both"
: data.show_current
? "show_current"
: "show_forecast";
return html`
<div class="card-config">
<ha-entity-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.required"
)})"
<ha-form
.hass=${this.hass}
.value=${this._entity}
.configValue=${"entity"}
.includeDomains=${includeDomains}
@change=${this._valueChanged}
allow-custom-entity
></ha-entity-picker>
<div class="side-by-side">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.name"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._name}
.configValue=${"name"}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></paper-input>
<hui-theme-select-editor
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
<ha-entity-attribute-picker
.hass=${this.hass}
.entityId=${this._entity}
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.secondary_info_attribute"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._secondary_info_attribute}
.configValue=${"secondary_info_attribute"}
@value-changed=${this._valueChanged}
></ha-entity-attribute-picker>
</div>
<div class="side-by-side">
<ha-formfield
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.weather-forecast.show_both"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-radio
.disabled=${this._has_forecast === false}
.checked=${this._has_forecast === true &&
this._show_current &&
this._show_forecast}
.configValue=${"show_both"}
@change=${this._valueChanged}
></ha-radio
></ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.weather-forecast.show_only_current"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-radio
.disabled=${this._has_forecast === false}
.checked=${this._has_forecast === false ||
(this._show_current && !this._show_forecast)}
.configValue=${"show_current"}
@change=${this._valueChanged}
></ha-radio
></ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.weather-forecast.show_only_forecast"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-radio
.disabled=${this._has_forecast === false}
.checked=${this._has_forecast === true &&
!this._show_current &&
this._show_forecast}
.configValue=${"show_forecast"}
@change=${this._valueChanged}
></ha-radio
></ha-formfield>
</div>
</div>
></ha-form>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.currentTarget! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (target.configValue.startsWith("show_")) {
this._config = { ...this._config };
if (target.configValue === "show_both") {
/* delete since true is default */
delete this._config.show_current;
delete this._config.show_forecast;
} else if (target.configValue === "show_current") {
delete this._config.show_current;
this._config.show_forecast = false;
} else if (target.configValue === "show_forecast") {
delete this._config.show_forecast;
this._config.show_current = false;
}
} else if (target.value === "") {
this._config = { ...this._config };
delete this._config[target.configValue!];
private _valueChanged(ev: CustomEvent): void {
const config = ev.detail.value;
if (config.forecast === "show_both") {
config.show_current = true;
config.show_forecast = true;
} else if (config.forecast === "show_current") {
config.show_current = true;
config.show_forecast = false;
} else {
this._config = {
...this._config,
[target.configValue!]:
target.checked !== undefined ? target.checked : target.value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
config.show_current = false;
config.show_forecast = true;
}
static get styles(): CSSResultGroup {
return configElementStyle;
delete config.forecast;
fireEvent(this, "config-changed", { config });
}
private _computeLabelCallback = (schema: HaFormSchema) => {
if (schema.name === "entity") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.required"
)})`;
}
return (
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) ||
this.hass!.localize(
`ui.panel.lovelace.editor.card.weather_forecast.${schema.name}`
)
);
};
}
declare global {

View File

@@ -16,7 +16,7 @@ export class HuiDialogEditLovelace extends LitElement {
@state() private _lovelace?: Lovelace;
private _config?: LovelaceConfig;
@state() private _config?: LovelaceConfig;
private _saving = false;

View File

@@ -1,10 +1,9 @@
import "@polymer/paper-input/paper-input";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import "../../../../components/ha-textfield";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { LovelaceConfig } from "../../../../data/lovelace";
import { HomeAssistant } from "../../../../types";
import { configElementStyle } from "../config-elements/config-elements-style";
import { EditorTarget } from "../types";
declare global {
@@ -30,16 +29,14 @@ export class HuiLovelaceEditor extends LitElement {
protected render(): TemplateResult {
return html`
<div class="card-config">
<paper-input
<ha-textfield
.label=${this.hass.localize(
"ui.panel.lovelace.editor.edit_lovelace.title"
)}
.value=${this._title}
.configValue=${"title"}
@value-changed=${this._valueChanged}
></paper-input>
</div>
@change=${this._valueChanged}
></ha-textfield>
`;
}
@@ -66,9 +63,11 @@ export class HuiLovelaceEditor extends LitElement {
fireEvent(this, "lovelace-config-changed", { config: newConfig });
}
static get styles(): CSSResultGroup {
return configElementStyle;
static styles: CSSResultGroup = css`
ha-textfield {
display: block;
}
`;
}
declare global {

View File

@@ -1,24 +1,17 @@
import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import "../../../../components/ha-form/ha-form";
import { 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 { stopPropagation } from "../../../../common/dom/stop_propagation";
import { slugify } from "../../../../common/string/slugify";
import "../../../../components/ha-formfield";
import "../../../../components/ha-icon-picker";
import "../../../../components/ha-select";
import "../../../../components/ha-switch";
import { LovelaceViewConfig } from "../../../../data/lovelace";
import { HomeAssistant } from "../../../../types";
import "../../components/hui-theme-select-editor";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type { LovelaceViewConfig } from "../../../../data/lovelace";
import type { HomeAssistant } from "../../../../types";
import {
DEFAULT_VIEW_LAYOUT,
PANEL_VIEW_LAYOUT,
SIDEBAR_VIEW_LAYOUT,
} from "../../views/const";
import { configElementStyle } from "../config-elements/config-elements-style";
import { EditorTarget } from "../types";
declare global {
interface HASSDomEvents {
@@ -38,32 +31,35 @@ export class HuiViewEditor extends LitElement {
private _suggestedPath = false;
get _path(): string {
if (!this._config) {
return "";
}
return this._config.path || "";
}
private _schema = memoizeOne((localize): HaFormSchema[] => [
{ name: "title", selector: { text: {} } },
{
name: "icon",
selector: {
icon: {},
},
},
{ name: "path", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
{
name: "type",
selector: {
select: {
options: [
DEFAULT_VIEW_LAYOUT,
SIDEBAR_VIEW_LAYOUT,
PANEL_VIEW_LAYOUT,
].map((type) => ({
value: type,
label: localize(`ui.panel.lovelace.editor.edit_view.types.${type}`),
})),
},
},
},
]);
get _title(): string {
if (!this._config) {
return "";
}
return this._config.title || "";
}
get _icon(): string {
if (!this._config) {
return "";
}
return this._config.icon || "";
}
get _theme(): string {
if (!this._config) {
return "";
}
return this._config.theme || "Backend-selected";
set config(config: LovelaceViewConfig) {
this._config = config;
}
get _type(): string {
@@ -75,142 +71,59 @@ export class HuiViewEditor extends LitElement {
: this._config.type || DEFAULT_VIEW_LAYOUT;
}
set config(config: LovelaceViewConfig) {
this._config = config;
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const schema = this._schema(this.hass.localize);
const data = {
theme: "Backend-selected",
...this._config,
type: this._type,
};
return html`
<div class="card-config">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.title"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._title}
.configValue=${"title"}
@value-changed=${this._valueChanged}
@blur=${this._handleTitleBlur}
></paper-input>
<ha-icon-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.icon"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._icon}
.placeholder=${this._icon}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
></ha-icon-picker>
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.url"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._path}
.configValue=${"path"}
@value-changed=${this._valueChanged}
></paper-input>
<hui-theme-select-editor
<ha-form
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></hui-theme-select-editor>
<ha-select
.label=${this.hass.localize(
"ui.panel.lovelace.editor.edit_view.type"
)}
.value=${this._type}
@selected=${this._typeChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${[DEFAULT_VIEW_LAYOUT, SIDEBAR_VIEW_LAYOUT, PANEL_VIEW_LAYOUT].map(
(type) => html`<mwc-list-item .value=${type}>
${this.hass.localize(
`ui.panel.lovelace.editor.edit_view.types.${type}`
)}
</mwc-list-item>`
)}
</ha-select>
</div>
></ha-form>
`;
}
private _valueChanged(ev: Event): void {
const target = ev.currentTarget! as EditorTarget;
private _valueChanged(ev: CustomEvent): void {
const config = ev.detail.value;
if (this[`_${target.configValue}`] === target.value) {
return;
if (config.type === "masonry") {
delete config.type;
}
let newConfig;
if (target.configValue) {
newConfig = {
...this._config,
[target.configValue!]:
target.checked !== undefined ? target.checked : target.value,
};
}
fireEvent(this, "view-config-changed", { config: newConfig });
}
private _typeChanged(ev): void {
const selected = ev.target.value;
if (selected === "") {
return;
}
const newConfig = {
...this._config,
};
delete newConfig.panel;
if (selected === "masonry") {
delete newConfig.type;
} else {
newConfig.type = selected;
}
fireEvent(this, "view-config-changed", { config: newConfig });
}
private _handleTitleBlur(ev) {
if (
!this.isNew ||
this._suggestedPath ||
this._config.path ||
!ev.currentTarget.value
this.isNew &&
!this._suggestedPath &&
config.title &&
(!this._config.path ||
config.path === slugify(this._config.title || "", "-"))
) {
return;
config.path = slugify(config.title, "-");
}
const config = {
...this._config,
path: slugify(ev.currentTarget.value, "-"),
};
fireEvent(this, "view-config-changed", { config });
}
static get styles(): CSSResultGroup {
return [
configElementStyle,
css`
.panel {
color: var(--secondary-text-color);
display: block;
}
`,
];
private _computeLabelCallback = (schema: HaFormSchema) => {
if (schema.name === "path") {
return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.url`);
}
return (
this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
) || this.hass.localize("ui.panel.lovelace.editor.edit_view.type")
);
};
}
declare global {

View File

@@ -9,13 +9,17 @@ import {
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-date-input";
import { UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity";
import { setInputDateTimeValue } from "../../../data/input_datetime";
import {
setInputDateTimeValue,
stateToIsoDateString,
} from "../../../data/input_datetime";
import type { HomeAssistant } from "../../../types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { EntityConfig, LovelaceRow } from "./types";
import "../../../components/ha-time-input";
import { computeStateName } from "../../../common/entity/compute_state_name";
@customElement("hui-input-datetime-entity-row")
class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
@@ -49,14 +53,22 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
`;
}
const name = this._config.name || computeStateName(stateObj);
return html`
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
<hui-generic-entity-row
.hass=${this.hass}
.config=${this._config}
.hideName=${stateObj.attributes.has_date &&
stateObj.attributes.has_time}
>
${stateObj.attributes.has_date
? html`
<ha-date-input
.label=${stateObj.attributes.has_time ? name : undefined}
.locale=${this.hass.locale}
.disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
.value=${`${stateObj.attributes.year}-${stateObj.attributes.month}-${stateObj.attributes.day}`}
.value=${stateToIsoDateString(stateObj)}
@value-changed=${this._dateChanged}
>
</ha-date-input>

View File

@@ -1,12 +1,5 @@
import "@material/mwc-list/mwc-list-item";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeStateName } from "../../../common/entity/compute_state_name";
@@ -87,8 +80,7 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow {
`;
}
static get styles(): CSSResultGroup {
return css`
static styles = css`
hui-generic-entity-row {
display: flex;
align-items: center;
@@ -97,7 +89,6 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow {
width: 100%;
}
`;
}
private _selectedChanged(ev): void {
const stateObj = this.hass!.states[this._config!.entity];

View File

@@ -1,4 +1,4 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { UNAVAILABLE, UNAVAILABLE_STATES } from "../../../data/entity";
import { setValue } from "../../../data/input_text";
@@ -8,6 +8,7 @@ import "../components/hui-generic-entity-row";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import { EntityConfig, LovelaceRow } from "./types";
import "../../../components/ha-textfield";
import { computeStateName } from "../../../common/entity/compute_state_name";
@customElement("hui-input-text-entity-row")
class HuiInputTextEntityRow extends LitElement implements LovelaceRow {
@@ -42,8 +43,13 @@ class HuiInputTextEntityRow extends LitElement implements LovelaceRow {
}
return html`
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
<hui-generic-entity-row
.hass=${this.hass}
.config=${this._config}
hideName
>
<ha-textfield
.label=${this._config.name || computeStateName(stateObj)}
.disabled=${stateObj.state === UNAVAILABLE}
.value=${stateObj.state}
.minlength=${stateObj.attributes.min}
@@ -75,6 +81,16 @@ class HuiInputTextEntityRow extends LitElement implements LovelaceRow {
ev.target.blur();
}
static styles = css`
hui-generic-entity-row {
display: flex;
align-items: center;
}
ha-textfield {
width: 100%;
}
`;
}
declare global {

View File

@@ -23,6 +23,18 @@ class HuiTimerEntityRow extends LitElement {
throw new Error("Invalid configuration");
}
this._config = config;
if (!this.hass) {
return;
}
const stateObj = this.hass!.states[this._config.entity];
if (stateObj) {
this._startInterval(stateObj);
} else {
this._clearInterval();
}
}
public disconnectedCallback(): void {
@@ -75,7 +87,9 @@ class HuiTimerEntityRow extends LitElement {
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("hass")) {
if (!this._config || !changedProps.has("hass")) {
return;
}
const stateObj = this.hass!.states[this._config!.entity];
const oldHass = changedProps.get("hass") as this["hass"];
const oldStateObj = oldHass
@@ -88,7 +102,6 @@ class HuiTimerEntityRow extends LitElement {
this._clearInterval();
}
}
}
private _clearInterval(): void {
if (this._interval) {

View File

@@ -69,7 +69,10 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
this._createCards();
}
if (!changedProperties.has("lovelace")) {
if (
!changedProperties.has("lovelace") &&
!changedProperties.has("_config")
) {
return;
}

View File

@@ -9,6 +9,8 @@ import {
import { ResolvedMediaSource } from "../../data/media_source";
import { HomeAssistant } from "../../types";
export const ERR_UNSUPPORTED_MEDIA = "Unsupported Media";
export class BrowserMediaPlayer {
private player: HTMLAudioElement;
@@ -25,6 +27,10 @@ export class BrowserMediaPlayer {
private onChange: () => void
) {
const player = new Audio(this.resolved.url);
if (player.canPlayType(resolved.mime_type) === "") {
throw new Error(ERR_UNSUPPORTED_MEDIA);
}
player.autoplay = true;
player.volume = volume;
player.addEventListener("play", this._handleChange);
player.addEventListener("playing", () => {
@@ -33,15 +39,7 @@ export class BrowserMediaPlayer {
});
player.addEventListener("pause", this._handleChange);
player.addEventListener("ended", this._handleChange);
player.addEventListener("canplaythrough", () => {
if (this._removed) {
return;
}
if (this.buffering) {
player.play();
}
this.onChange();
});
player.addEventListener("canplaythrough", this._handleChange);
this.player = player;
}

View File

@@ -49,9 +49,13 @@ import {
SUPPORT_VOLUME_SET,
} from "../../data/media-player";
import { ResolvedMediaSource } from "../../data/media_source";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../types";
import "../lovelace/components/hui-marquee";
import { BrowserMediaPlayer } from "./browser-media-player";
import {
BrowserMediaPlayer,
ERR_UNSUPPORTED_MEDIA,
} from "./browser-media-player";
declare global {
interface HASSDomEvents {
@@ -125,6 +129,7 @@ export class BarMediaPlayer extends LitElement {
throw Error("Only browser supported");
}
this._tearDownBrowserPlayer();
try {
this._browserPlayer = new BrowserMediaPlayer(
this.hass,
item,
@@ -132,6 +137,17 @@ export class BarMediaPlayer extends LitElement {
this._browserPlayerVolume,
() => this.requestUpdate("_browserPlayer")
);
} catch (err: any) {
if (err.message === ERR_UNSUPPORTED_MEDIA) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.components.media-browser.media_not_supported"
),
});
} else {
throw err;
}
}
this._newMediaExpected = false;
}
@@ -264,7 +280,9 @@ export class BarMediaPlayer extends LitElement {
return html`
<div class="choose-player ${isBrowser ? "browser" : ""}">
${
stateObj && supportsFeature(stateObj, SUPPORT_VOLUME_SET)
!this.narrow &&
stateObj &&
supportsFeature(stateObj, SUPPORT_VOLUME_SET)
? html`
<ha-button-menu corner="BOTTOM_START" y="0" x="76">
<ha-icon-button
@@ -601,6 +619,7 @@ export class BarMediaPlayer extends LitElement {
img {
max-height: 100px;
max-width: 100px;
}
.app img {
@@ -613,8 +632,8 @@ export class BarMediaPlayer extends LitElement {
}
:host([narrow]) {
min-height: 80px;
max-height: 80px;
min-height: 56px;
max-height: 56px;
}
:host([narrow]) .controls-progress {
@@ -622,13 +641,19 @@ export class BarMediaPlayer extends LitElement {
min-width: 48px;
}
:host([narrow]) .media-info {
padding-left: 8px;
}
:host([narrow]) .controls {
display: flex;
padding-bottom: 0;
--mdc-icon-size: 30px;
}
:host([narrow]) .choose-player {
padding-left: 0;
padding-right: 8px;
min-width: 48px;
flex: unset;
justify-content: center;
@@ -636,16 +661,16 @@ export class BarMediaPlayer extends LitElement {
:host([narrow]) .choose-player.browser {
justify-content: flex-end;
width: 100%;
}
:host([narrow]) img {
max-height: 80px;
max-height: 56px;
max-width: 56px;
}
:host([narrow]) .blank-image {
height: 80px;
width: 80px;
height: 56px;
width: 56px;
}
:host([narrow]) mwc-linear-progress {

View File

@@ -27,7 +27,10 @@ import {
MediaPickedEvent,
MediaPlayerItem,
} from "../../data/media-player";
import { resolveMediaSource } from "../../data/media_source";
import {
ResolvedMediaSource,
resolveMediaSource,
} from "../../data/media_source";
import "../../layouts/ha-app-layout";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant, Route } from "../../types";
@@ -224,11 +227,19 @@ class PanelMediaBrowser extends LitElement {
}
this._player.showResolvingNewMediaPicked();
const resolvedUrl = await resolveMediaSource(
this.hass,
item.media_content_id
);
let resolvedUrl: ResolvedMediaSource;
try {
resolvedUrl = await resolveMediaSource(this.hass, item.media_content_id);
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.components.media-browser.media_browsing_error"
),
text: err.message,
});
this._player.hideResolvingNewMediaPicked();
return;
}
if (resolvedUrl.mime_type.startsWith("audio/")) {
this._player.playItem(item, resolvedUrl);

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