-
${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.edit"
- )}
@@ -92,7 +95,11 @@ export class HuiCardOptions extends LitElement {
@click="${this._cardUp}"
?disabled="${this.path![1] === 0}"
>
-
+
Entity not available: [[config.entity]]
+
${this.hass.localize(
+ "ui.panel.lovelace.warning.entity_not_found",
+ "entity",
+ this.config.entity
+ )}
`;
}
@@ -107,11 +115,6 @@ class HuiGenericEntityRow extends LitElement {
display: block;
color: var(--secondary-text-color);
}
- .not-found {
- flex: 1;
- background-color: yellow;
- padding: 8px;
- }
state-badge {
flex: 0 0 40px;
}
diff --git a/src/panels/lovelace/components/hui-image.js b/src/panels/lovelace/components/hui-image.js
deleted file mode 100644
index 1ad89c3bac..0000000000
--- a/src/panels/lovelace/components/hui-image.js
+++ /dev/null
@@ -1,199 +0,0 @@
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-import "@polymer/paper-toggle-button/paper-toggle-button";
-
-import { STATES_OFF } from "../../../common/const";
-import LocalizeMixin from "../../../mixins/localize-mixin";
-
-import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
-
-const UPDATE_INTERVAL = 10000;
-const DEFAULT_FILTER = "grayscale(100%)";
-
-/*
- * @appliesMixin LocalizeMixin
- */
-class HuiImage extends LocalizeMixin(PolymerElement) {
- static get template() {
- return html`
- ${this.styleTemplate}
-
-

-
-
- `;
- }
-
- static get styleTemplate() {
- return html`
-
- `;
- }
-
- static get properties() {
- return {
- hass: {
- type: Object,
- observer: "_hassChanged",
- },
- entity: String,
- image: String,
- stateImage: Object,
- cameraImage: String,
- aspectRatio: String,
- filter: String,
- stateFilter: Object,
- _imageSrc: String,
- };
- }
-
- static get observers() {
- return ["_configChanged(image, stateImage, cameraImage, aspectRatio)"];
- }
-
- connectedCallback() {
- super.connectedCallback();
- if (this.cameraImage) {
- this.timer = setInterval(
- () => this._updateCameraImageSrc(),
- UPDATE_INTERVAL
- );
- }
- }
-
- disconnectedCallback() {
- super.disconnectedCallback();
- clearInterval(this.timer);
- }
-
- _configChanged(image, stateImage, cameraImage, aspectRatio) {
- const ratio = parseAspectRatio(aspectRatio);
-
- if (ratio && ratio.w > 0 && ratio.h > 0) {
- this.$.wrapper.style.paddingBottom = `${(
- (100 * ratio.h) /
- ratio.w
- ).toFixed(2)}%`;
- this.$.wrapper.classList.add("ratio");
- }
-
- if (cameraImage) {
- this._updateCameraImageSrc();
- } else if (image && !stateImage) {
- this._imageSrc = image;
- }
- }
-
- _onImageError() {
- this._imageSrc = null;
- this.$.image.classList.add("hidden");
- if (!this.$.wrapper.classList.contains("ratio")) {
- this.$.brokenImage.style.setProperty(
- "height",
- `${this._lastImageHeight || "100"}px`
- );
- }
- this.$.brokenImage.classList.remove("hidden");
- }
-
- _onImageLoad() {
- this.$.image.classList.remove("hidden");
- this.$.brokenImage.classList.add("hidden");
- if (!this.$.wrapper.classList.contains("ratio")) {
- this._lastImageHeight = this.$.image.offsetHeight;
- }
- }
-
- _hassChanged(hass) {
- if (this.cameraImage || !this.entity) {
- return;
- }
-
- const stateObj = hass.states[this.entity];
- const newState = !stateObj ? "unavailable" : stateObj.state;
-
- if (newState === this._currentState) return;
- this._currentState = newState;
-
- this._updateStateImage();
- this._updateStateFilter(stateObj);
- }
-
- _updateStateImage() {
- if (!this.stateImage) {
- this._imageFallback = true;
- return;
- }
- const stateImg = this.stateImage[this._currentState];
- this._imageSrc = stateImg || this.image;
- this._imageFallback = !stateImg;
- }
-
- _updateStateFilter(stateObj) {
- let filter;
- if (!this.stateFilter) {
- filter = this.filter;
- } else {
- filter = this.stateFilter[this._currentState] || this.filter;
- }
-
- const isOff = !stateObj || STATES_OFF.includes(stateObj.state);
- this.$.image.style.filter =
- filter || (isOff && this._imageFallback && DEFAULT_FILTER) || "";
- }
-
- async _updateCameraImageSrc() {
- try {
- const { content_type: contentType, content } = await this.hass.callWS({
- type: "camera_thumbnail",
- entity_id: this.cameraImage,
- });
- this._imageSrc = `data:${contentType};base64, ${content}`;
- this._onImageLoad();
- } catch (err) {
- this._onImageError();
- }
- }
-}
-
-customElements.define("hui-image", HuiImage);
diff --git a/src/panels/lovelace/components/hui-image.ts b/src/panels/lovelace/components/hui-image.ts
new file mode 100644
index 0000000000..04a12f34d8
--- /dev/null
+++ b/src/panels/lovelace/components/hui-image.ts
@@ -0,0 +1,225 @@
+import "@polymer/paper-toggle-button/paper-toggle-button";
+
+import { STATES_OFF } from "../../../common/const";
+
+import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
+import {
+ LitElement,
+ TemplateResult,
+ html,
+ property,
+ CSSResult,
+ css,
+ PropertyValues,
+ query,
+} from "lit-element";
+import { HomeAssistant } from "../../../types";
+import { styleMap } from "lit-html/directives/style-map";
+import { classMap } from "lit-html/directives/class-map";
+import { b64toBlob } from "../../../common/file/b64-to-blob";
+import { fetchThumbnail } from "../../../data/camera";
+
+const UPDATE_INTERVAL = 10000;
+const DEFAULT_FILTER = "grayscale(100%)";
+
+export interface StateSpecificConfig {
+ [state: string]: string;
+}
+
+/*
+ * @appliesMixin LocalizeMixin
+ */
+class HuiImage extends LitElement {
+ @property() public hass?: HomeAssistant;
+ @property() public entity?: string;
+ @property() public image?: string;
+ @property() public stateImage?: StateSpecificConfig;
+ @property() public cameraImage?: string;
+ @property() public aspectRatio?: string;
+ @property() public filter?: string;
+ @property() public stateFilter?: StateSpecificConfig;
+
+ @property() private _loadError?: boolean;
+ @property() private _cameraImageSrc?: string;
+ @query("img") private _image!: HTMLImageElement;
+ private _lastImageHeight?: number;
+ private _cameraUpdater?: number;
+ private _attached?: boolean;
+
+ public connectedCallback() {
+ super.connectedCallback();
+ this._attached = true;
+ this._startUpdateCameraInterval();
+ }
+
+ public disconnectedCallback() {
+ super.disconnectedCallback();
+ this._attached = false;
+ this._stopUpdateCameraInterval();
+ }
+
+ protected render(): TemplateResult | void {
+ const ratio = this.aspectRatio ? parseAspectRatio(this.aspectRatio) : null;
+ const stateObj =
+ this.hass && this.entity ? this.hass.states[this.entity] : undefined;
+ const state = stateObj ? stateObj.state : "unavailable";
+
+ // Figure out image source to use
+ let imageSrc: string | undefined;
+ // Track if we are we using a fallback image, used for filter.
+ let imageFallback = !this.stateImage;
+
+ if (this.cameraImage) {
+ imageSrc = this._cameraImageSrc;
+ } else if (this.stateImage) {
+ const stateImage = this.stateImage[state];
+
+ if (stateImage) {
+ imageSrc = stateImage;
+ } else {
+ imageSrc = this.image;
+ imageFallback = true;
+ }
+ } else {
+ imageSrc = this.image;
+ }
+
+ // Figure out filter to use
+ let filter = this.filter || "";
+
+ if (this.stateFilter && this.stateFilter[state]) {
+ filter = this.stateFilter[state];
+ }
+
+ if (!filter && this.entity) {
+ const isOff = !stateObj || STATES_OFF.includes(state);
+ filter = isOff && imageFallback ? DEFAULT_FILTER : "";
+ }
+
+ return html`
+
0 && ratio.h > 0
+ ? `${((100 * ratio.h) / ratio.w).toFixed(2)}%`
+ : "",
+ })}
+ class=${classMap({
+ ratio: Boolean(ratio && ratio.w > 0 && ratio.h > 0),
+ })}
+ >
+

+
+
+ `;
+ }
+
+ protected updated(changedProps: PropertyValues): void {
+ if (changedProps.has("cameraImage")) {
+ this._updateCameraImageSrc();
+ this._startUpdateCameraInterval();
+ return;
+ }
+ }
+
+ private _startUpdateCameraInterval() {
+ this._stopUpdateCameraInterval();
+ if (this.cameraImage && this._attached) {
+ this._cameraUpdater = window.setInterval(
+ () => this._updateCameraImageSrc(),
+ UPDATE_INTERVAL
+ );
+ }
+ }
+
+ private _stopUpdateCameraInterval() {
+ if (this._cameraUpdater) {
+ clearInterval(this._cameraUpdater);
+ }
+ }
+
+ private _onImageError() {
+ this._loadError = true;
+ }
+
+ private async _onImageLoad() {
+ this._loadError = false;
+ await this.updateComplete;
+ this._lastImageHeight = this._image.offsetHeight;
+ }
+
+ private async _updateCameraImageSrc() {
+ if (!this.hass || !this.cameraImage) {
+ return;
+ }
+ try {
+ const { content_type: contentType, content } = await fetchThumbnail(
+ this.hass,
+ this.cameraImage
+ );
+ if (this._cameraImageSrc) {
+ URL.revokeObjectURL(this._cameraImageSrc);
+ }
+ this._cameraImageSrc = URL.createObjectURL(
+ b64toBlob(content, contentType)
+ );
+ this._onImageLoad();
+ } catch (err) {
+ this._onImageError();
+ }
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ img {
+ display: block;
+ height: auto;
+ transition: filter 0.2s linear;
+ width: 100%;
+ }
+
+ .ratio {
+ position: relative;
+ width: 100%;
+ height: 0;
+ }
+
+ .ratio img,
+ .ratio div {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ }
+
+ #brokenImage {
+ background: grey url("/static/images/image-broken.svg") center/36px
+ no-repeat;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-image": HuiImage;
+ }
+}
+
+customElements.define("hui-image", HuiImage);
diff --git a/src/panels/lovelace/components/hui-theme-select-editor.ts b/src/panels/lovelace/components/hui-theme-select-editor.ts
index 98bc7687ad..318e42add8 100644
--- a/src/panels/lovelace/components/hui-theme-select-editor.ts
+++ b/src/panels/lovelace/components/hui-theme-select-editor.ts
@@ -4,7 +4,7 @@ import {
PropertyDeclarations,
TemplateResult,
} from "lit-element";
-import "@polymer/paper-button/paper-button";
+import "@material/mwc-button";
import { HomeAssistant } from "../../../types";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
diff --git a/src/panels/lovelace/components/hui-warning.ts b/src/panels/lovelace/components/hui-warning.ts
new file mode 100644
index 0000000000..7bbbe1af67
--- /dev/null
+++ b/src/panels/lovelace/components/hui-warning.ts
@@ -0,0 +1,34 @@
+import {
+ html,
+ LitElement,
+ TemplateResult,
+ CSSResult,
+ css,
+ customElement,
+} from "lit-element";
+
+@customElement("hui-warning")
+export class HuiWarning extends LitElement {
+ protected render(): TemplateResult | void {
+ return html`
+
+ `;
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ :host {
+ display: block;
+ color: black;
+ background-color: #fce588;
+ padding: 8px;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-warning": HuiWarning;
+ }
+}
diff --git a/src/panels/lovelace/components/hui-yaml-editor.ts b/src/panels/lovelace/components/hui-yaml-editor.ts
index ae2a9b65ad..c0709b837c 100644
--- a/src/panels/lovelace/components/hui-yaml-editor.ts
+++ b/src/panels/lovelace/components/hui-yaml-editor.ts
@@ -3,7 +3,10 @@ import CodeMirror from "codemirror";
import "codemirror/mode/yaml/yaml";
// @ts-ignore
import codeMirrorCSS from "codemirror/lib/codemirror.css";
+import { HomeAssistant } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
+import { computeRTL } from "../../../common/util/compute_rtl";
+
declare global {
interface HASSDomEvents {
"yaml-changed": {
@@ -14,6 +17,7 @@ declare global {
}
export class HuiYamlEditor extends HTMLElement {
+ public _hass?: HomeAssistant;
public codemirror: CodeMirror;
private _value: string;
@@ -26,25 +30,42 @@ export class HuiYamlEditor extends HTMLElement {
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
`;
}
+ set hass(hass: HomeAssistant) {
+ this._hass = hass;
+ if (this._hass) {
+ this.setScrollBarDirection();
+ }
+ }
+
set value(value: string) {
if (this.codemirror) {
if (value !== this.codemirror.getValue()) {
@@ -70,13 +91,13 @@ export class HuiYamlEditor extends HTMLElement {
mode: "yaml",
tabSize: 2,
autofocus: true,
- extraKeys: {
- Tab: (cm: CodeMirror) => {
- const spaces = Array(cm.getOption("indentUnit") + 1).join(" ");
- cm.replaceSelection(spaces);
- },
- },
+ viewportMargin: Infinity,
+ gutters:
+ this._hass && computeRTL(this._hass!)
+ ? ["rtl-gutter", "CodeMirror-linenumbers"]
+ : [],
});
+ this.setScrollBarDirection();
this.codemirror.on("changes", () => this._onChange());
} else {
this.codemirror.refresh();
@@ -86,6 +107,16 @@ export class HuiYamlEditor extends HTMLElement {
private _onChange(): void {
fireEvent(this, "yaml-changed", { value: this.codemirror.getValue() });
}
+
+ private setScrollBarDirection() {
+ if (!this.codemirror) {
+ return;
+ }
+
+ this.codemirror
+ .getWrapperElement()
+ .classList.toggle("rtl", computeRTL(this._hass!));
+ }
}
declare global {
diff --git a/src/panels/lovelace/components/notifications/hui-configurator-notification-item.js b/src/panels/lovelace/components/notifications/hui-configurator-notification-item.js
index 0a480f4332..020a4b2821 100644
--- a/src/panels/lovelace/components/notifications/hui-configurator-notification-item.js
+++ b/src/panels/lovelace/components/notifications/hui-configurator-notification-item.js
@@ -1,4 +1,4 @@
-import "@polymer/paper-button/paper-button";
+import "@material/mwc-button";
import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
@@ -23,8 +23,8 @@ export class HuiConfiguratorNotificationItem extends EventsMixin(
[[_getMessage(notification)]]
-
[[_localizeState(notification.state)]][[_localizeState(notification.state)]]
`;
diff --git a/src/panels/lovelace/components/notifications/hui-notification-drawer.js b/src/panels/lovelace/components/notifications/hui-notification-drawer.js
index b956eb9a81..4cb33646f4 100644
--- a/src/panels/lovelace/components/notifications/hui-notification-drawer.js
+++ b/src/panels/lovelace/components/notifications/hui-notification-drawer.js
@@ -1,4 +1,4 @@
-import "@polymer/paper-button/paper-button";
+import "@material/mwc-button";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/app-layout/app-toolbar/app-toolbar";
diff --git a/src/panels/lovelace/components/notifications/hui-notification-item-template.js b/src/panels/lovelace/components/notifications/hui-notification-item-template.js
index f949c9e749..a7b091849a 100644
--- a/src/panels/lovelace/components/notifications/hui-notification-item-template.js
+++ b/src/panels/lovelace/components/notifications/hui-notification-item-template.js
@@ -1,4 +1,4 @@
-import "@polymer/paper-button/paper-button";
+import "@material/mwc-button";
import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
diff --git a/src/panels/lovelace/components/notifications/hui-notifications-button.js b/src/panels/lovelace/components/notifications/hui-notifications-button.js
deleted file mode 100644
index ddd8cc8ad6..0000000000
--- a/src/panels/lovelace/components/notifications/hui-notifications-button.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import "@polymer/paper-button/paper-button";
-import "@polymer/paper-icon-button/paper-icon-button";
-import "@polymer/app-layout/app-toolbar/app-toolbar";
-
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-
-import EventsMixin from "../../../../mixins/events-mixin";
-
-/*
- * @appliesMixin EventsMixin
- */
-export class HuiNotificationsButton extends EventsMixin(PolymerElement) {
- static get template() {
- return html`
-
-
-
- `;
- }
-
- static get properties() {
- return {
- open: {
- type: Boolean,
- notify: true,
- },
- notifications: {
- type: Array,
- value: [],
- },
- };
- }
-
- _clicked() {
- this.open = true;
- }
-
- _hasNotifications(notifications) {
- return notifications.length > 0;
- }
-}
-customElements.define("hui-notifications-button", HuiNotificationsButton);
diff --git a/src/panels/lovelace/components/notifications/hui-notifications-button.ts b/src/panels/lovelace/components/notifications/hui-notifications-button.ts
new file mode 100644
index 0000000000..c56d2931a1
--- /dev/null
+++ b/src/panels/lovelace/components/notifications/hui-notifications-button.ts
@@ -0,0 +1,80 @@
+import {
+ html,
+ LitElement,
+ TemplateResult,
+ css,
+ CSSResult,
+ property,
+} from "lit-element";
+import "@polymer/paper-icon-button/paper-icon-button";
+import { fireEvent } from "../../../../common/dom/fire_event";
+
+declare global {
+ // tslint:disable-next-line
+ interface HASSDomEvents {
+ "opened-changed": { value: boolean };
+ }
+}
+
+class HuiNotificationsButton extends LitElement {
+ @property() public notifications?: string[];
+ @property() public opened?: boolean;
+
+ protected render(): TemplateResult | void {
+ return html`
+
+ ${this.notifications && this.notifications.length > 0
+ ? html`
+
+ ${this.notifications.length}
+
+ `
+ : ""}
+ `;
+ }
+
+ static get styles(): CSSResult[] {
+ return [
+ css`
+ :host {
+ position: relative;
+ }
+
+ .indicator {
+ position: absolute;
+ top: 0px;
+ right: -3px;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: var(--accent-color);
+ pointer-events: none;
+ z-index: 1;
+ }
+
+ .indicator > div {
+ right: 7px;
+ top: 3px;
+ position: absolute;
+ font-size: 0.55em;
+ }
+ `,
+ ];
+ }
+
+ private _clicked() {
+ this.opened = true;
+ fireEvent(this, "opened-changed", { value: this.opened });
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-notifications-button": HuiNotificationsButton;
+ }
+}
+
+customElements.define("hui-notifications-button", HuiNotificationsButton);
diff --git a/src/panels/lovelace/components/notifications/hui-persistent-notification-item.js b/src/panels/lovelace/components/notifications/hui-persistent-notification-item.js
index b4db796b4c..d763ab3838 100644
--- a/src/panels/lovelace/components/notifications/hui-persistent-notification-item.js
+++ b/src/panels/lovelace/components/notifications/hui-persistent-notification-item.js
@@ -1,4 +1,4 @@
-import "@polymer/paper-button/paper-button";
+import "@material/mwc-button";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-tooltip/paper-tooltip";
@@ -49,8 +49,8 @@ export class HuiPersistentNotificationItem extends LocalizeMixin(
-