mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-11-04 00:19:47 +00:00 
			
		
		
		
	Compare commits
	
		
			118 Commits
		
	
	
		
			20200904.0
			...
			fix-action
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					42f720496b | ||
| 
						 | 
					c64d88d8b5 | ||
| 
						 | 
					3030b8d476 | ||
| 
						 | 
					92ed14c0e4 | ||
| 
						 | 
					5b94a4de9a | ||
| 
						 | 
					709112c498 | ||
| 
						 | 
					e465ec8835 | ||
| 
						 | 
					f6eb31bf9d | ||
| 
						 | 
					426f939982 | ||
| 
						 | 
					fab6cebf0d | ||
| 
						 | 
					ff081dd0f1 | ||
| 
						 | 
					868399ed6f | ||
| 
						 | 
					1bc9b95289 | ||
| 
						 | 
					9af805ab5e | ||
| 
						 | 
					6b88081360 | ||
| 
						 | 
					50d37ce4f6 | ||
| 
						 | 
					af0246cd27 | ||
| 
						 | 
					857e4e49d8 | ||
| 
						 | 
					c1afed7f98 | ||
| 
						 | 
					5480e54185 | ||
| 
						 | 
					99d0a0a6fd | ||
| 
						 | 
					8a998369d6 | ||
| 
						 | 
					8b490c5047 | ||
| 
						 | 
					7e70ba6ab2 | ||
| 
						 | 
					90e09fc384 | ||
| 
						 | 
					266f2e763d | ||
| 
						 | 
					c979cfb912 | ||
| 
						 | 
					8ee29b1e43 | ||
| 
						 | 
					26fbc07cac | ||
| 
						 | 
					f01fe65be4 | ||
| 
						 | 
					3fdd6a80f9 | ||
| 
						 | 
					da1de8db1d | ||
| 
						 | 
					dd1bf7b49d | ||
| 
						 | 
					f18913b5a0 | ||
| 
						 | 
					a2cd227f1a | ||
| 
						 | 
					78e64e1f60 | ||
| 
						 | 
					23a9b79320 | ||
| 
						 | 
					76394ce341 | ||
| 
						 | 
					1935df1faa | ||
| 
						 | 
					5af4ce28ab | ||
| 
						 | 
					ce8ee569c4 | ||
| 
						 | 
					b0508f430e | ||
| 
						 | 
					2139a80a7a | ||
| 
						 | 
					aa4bc2ce03 | ||
| 
						 | 
					fa65f84e09 | ||
| 
						 | 
					c06357a351 | ||
| 
						 | 
					092a02a624 | ||
| 
						 | 
					b9699f745f | ||
| 
						 | 
					7fa9f10c30 | ||
| 
						 | 
					7bf0655dae | ||
| 
						 | 
					96c5fdcbeb | ||
| 
						 | 
					c2e6d40382 | ||
| 
						 | 
					810d2a1ceb | ||
| 
						 | 
					af74f21af9 | ||
| 
						 | 
					cdf7558a8e | ||
| 
						 | 
					41b86e6c10 | ||
| 
						 | 
					3039c678a5 | ||
| 
						 | 
					498882d014 | ||
| 
						 | 
					6c2b8c2abb | ||
| 
						 | 
					e955cc4378 | ||
| 
						 | 
					eb96dd4803 | ||
| 
						 | 
					e0bdef98a6 | ||
| 
						 | 
					1130007d14 | ||
| 
						 | 
					d99d092784 | ||
| 
						 | 
					e3b18a33ca | ||
| 
						 | 
					1890aab1e6 | ||
| 
						 | 
					42bf350034 | ||
| 
						 | 
					5ff52ea113 | ||
| 
						 | 
					432e3ba636 | ||
| 
						 | 
					f7ab52fe9a | ||
| 
						 | 
					ad8430049d | ||
| 
						 | 
					2dffe7ba9e | ||
| 
						 | 
					5b8f97e0f6 | ||
| 
						 | 
					b3a763a48d | ||
| 
						 | 
					07569f10b5 | ||
| 
						 | 
					7c5a78a1cf | ||
| 
						 | 
					0e021e7d7d | ||
| 
						 | 
					b30ee884a7 | ||
| 
						 | 
					869b7c85ca | ||
| 
						 | 
					4d0d1ed2a1 | ||
| 
						 | 
					291983e4c3 | ||
| 
						 | 
					909cff2158 | ||
| 
						 | 
					4e676b1dba | ||
| 
						 | 
					9149bb9333 | ||
| 
						 | 
					4631994f20 | ||
| 
						 | 
					82e9178320 | ||
| 
						 | 
					67b4688168 | ||
| 
						 | 
					6e0e169b6e | ||
| 
						 | 
					100ba8edfa | ||
| 
						 | 
					d7448ecb95 | ||
| 
						 | 
					8b1801f378 | ||
| 
						 | 
					01a4d57566 | ||
| 
						 | 
					7edc9064d9 | ||
| 
						 | 
					30c47a65f4 | ||
| 
						 | 
					0889f42a00 | ||
| 
						 | 
					f15fbe53cf | ||
| 
						 | 
					046f7b5153 | ||
| 
						 | 
					5339fe6e06 | ||
| 
						 | 
					de7ffb10cb | ||
| 
						 | 
					80224e6974 | ||
| 
						 | 
					0c7c536f73 | ||
| 
						 | 
					e5c386c39a | ||
| 
						 | 
					bb2462483e | ||
| 
						 | 
					d5bc498373 | ||
| 
						 | 
					979b7ae651 | ||
| 
						 | 
					c73330a466 | ||
| 
						 | 
					efe8eca4e3 | ||
| 
						 | 
					a37aad18b7 | ||
| 
						 | 
					cfa0c45213 | ||
| 
						 | 
					509481ef06 | ||
| 
						 | 
					9aa8175e23 | ||
| 
						 | 
					76f59d99a2 | ||
| 
						 | 
					bd66bd6cf0 | ||
| 
						 | 
					d69333dea4 | ||
| 
						 | 
					3fd7899b93 | ||
| 
						 | 
					8f8a2cea56 | ||
| 
						 | 
					879011c8e9 | ||
| 
						 | 
					d5794c3e2e | 
							
								
								
									
										60
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
name: "CodeQL"
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [dev, master]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    # The branches below must be a subset of the branches above
 | 
			
		||||
    branches: [dev]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  analyze:
 | 
			
		||||
    name: Analyze
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        # Override automatic language detection by changing the below list
 | 
			
		||||
        # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
 | 
			
		||||
        language: ['javascript']
 | 
			
		||||
        # Learn more...
 | 
			
		||||
        # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
    - name: Checkout repository
 | 
			
		||||
      uses: actions/checkout@v2
 | 
			
		||||
      with:
 | 
			
		||||
        # We must fetch at least the immediate parents so that if this is
 | 
			
		||||
        # a pull request then we can checkout the head.
 | 
			
		||||
        fetch-depth: 2
 | 
			
		||||
 | 
			
		||||
    # If this run was triggered by a pull request event, then checkout
 | 
			
		||||
    # the head of the pull request instead of the merge commit.
 | 
			
		||||
    - run: git checkout HEAD^2
 | 
			
		||||
      if: ${{ github.event_name == 'pull_request' }}
 | 
			
		||||
 | 
			
		||||
    # Initializes the CodeQL tools for scanning.
 | 
			
		||||
    - name: Initialize CodeQL
 | 
			
		||||
      uses: github/codeql-action/init@v1
 | 
			
		||||
      with:
 | 
			
		||||
        languages: ${{ matrix.language }}
 | 
			
		||||
 | 
			
		||||
    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
 | 
			
		||||
    # If this step fails, then you should remove it and run the build manually (see below)
 | 
			
		||||
    - name: Autobuild
 | 
			
		||||
      uses: github/codeql-action/autobuild@v1
 | 
			
		||||
 | 
			
		||||
    # ℹ️ Command-line programs to run using the OS shell.
 | 
			
		||||
    # 📚 https://git.io/JvXDl
 | 
			
		||||
 | 
			
		||||
    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
 | 
			
		||||
    #    and modify them (or add more) to build your code if your project
 | 
			
		||||
    #    uses a compiled language
 | 
			
		||||
 | 
			
		||||
    #- run: |
 | 
			
		||||
    #   make bootstrap
 | 
			
		||||
    #   make release
 | 
			
		||||
 | 
			
		||||
    - name: Perform CodeQL Analysis
 | 
			
		||||
      uses: github/codeql-action/analyze@v1
 | 
			
		||||
@@ -2,8 +2,8 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
 | 
			
		||||
/* eslint-plugin-disable lit */
 | 
			
		||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
 | 
			
		||||
import "../../../src/components/ha-card";
 | 
			
		||||
import "../../../src/dialogs/more-info/more-info-content";
 | 
			
		||||
import "../../../src/state-summary/state-card-content";
 | 
			
		||||
import "./more-info-content";
 | 
			
		||||
 | 
			
		||||
class DemoMoreInfo extends PolymerElement {
 | 
			
		||||
  static get template() {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										73
									
								
								gallery/src/components/more-info-content.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								gallery/src/components/more-info-content.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
import { HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
import { property, PropertyValues, UpdatingElement } from "lit-element";
 | 
			
		||||
import dynamicContentUpdater from "../../../src/common/dom/dynamic_content_updater";
 | 
			
		||||
import { stateMoreInfoType } from "../../../src/common/entity/state_more_info_type";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-alarm_control_panel";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-automation";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-camera";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-climate";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-configurator";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-counter";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-cover";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-default";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-fan";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-group";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-humidifier";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-input_datetime";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-light";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-lock";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-media_player";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-person";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-script";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-sun";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-timer";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-vacuum";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-water_heater";
 | 
			
		||||
import "../../../src/dialogs/more-info/controls/more-info-weather";
 | 
			
		||||
import { HomeAssistant } from "../../../src/types";
 | 
			
		||||
 | 
			
		||||
class MoreInfoContent extends UpdatingElement {
 | 
			
		||||
  @property({ attribute: false }) public hass?: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property() public stateObj?: HassEntity;
 | 
			
		||||
 | 
			
		||||
  private _detachedChild?: ChildNode;
 | 
			
		||||
 | 
			
		||||
  protected firstUpdated(): void {
 | 
			
		||||
    this.style.position = "relative";
 | 
			
		||||
    this.style.display = "block";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // This is not a lit element, but an updating element, so we implement update
 | 
			
		||||
  protected update(changedProps: PropertyValues): void {
 | 
			
		||||
    super.update(changedProps);
 | 
			
		||||
    const stateObj = this.stateObj;
 | 
			
		||||
    const hass = this.hass;
 | 
			
		||||
 | 
			
		||||
    if (!stateObj || !hass) {
 | 
			
		||||
      if (this.lastChild) {
 | 
			
		||||
        this._detachedChild = this.lastChild;
 | 
			
		||||
        // Detach child to prevent it from doing work.
 | 
			
		||||
        this.removeChild(this.lastChild);
 | 
			
		||||
      }
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this._detachedChild) {
 | 
			
		||||
      this.appendChild(this._detachedChild);
 | 
			
		||||
      this._detachedChild = undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const moreInfoType =
 | 
			
		||||
      stateObj.attributes && "custom_ui_more_info" in stateObj.attributes
 | 
			
		||||
        ? stateObj.attributes.custom_ui_more_info
 | 
			
		||||
        : "more-info-" + stateMoreInfoType(stateObj);
 | 
			
		||||
 | 
			
		||||
    dynamicContentUpdater(this, moreInfoType.toUpperCase(), {
 | 
			
		||||
      hass,
 | 
			
		||||
      stateObj,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define("more-info-content", MoreInfoContent);
 | 
			
		||||
@@ -3,10 +3,10 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
 | 
			
		||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
 | 
			
		||||
import "../../../src/components/ha-card";
 | 
			
		||||
import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";
 | 
			
		||||
import "../../../src/dialogs/more-info/more-info-content";
 | 
			
		||||
import { getEntity } from "../../../src/fake_data/entity";
 | 
			
		||||
import { provideHass } from "../../../src/fake_data/provide_hass";
 | 
			
		||||
import "../components/demo-more-infos";
 | 
			
		||||
import "../components/more-info-content";
 | 
			
		||||
 | 
			
		||||
const ENTITIES = [
 | 
			
		||||
  getEntity("light", "bed_light", "on", {
 | 
			
		||||
 
 | 
			
		||||
@@ -88,8 +88,8 @@ class HassioAddonNetwork extends LitElement {
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="card-actions">
 | 
			
		||||
          <ha-progress-button class="warning" @click=${this._resetTapped}>
 | 
			
		||||
            Reset to defaults </ha-progress-button
 | 
			
		||||
          >>
 | 
			
		||||
            Reset to defaults
 | 
			
		||||
          </ha-progress-button>
 | 
			
		||||
          <ha-progress-button @click=${this._saveTapped}>
 | 
			
		||||
            Save
 | 
			
		||||
          </ha-progress-button>
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ import "../../../src/components/ha-svg-icon";
 | 
			
		||||
import {
 | 
			
		||||
  extractApiErrorMessage,
 | 
			
		||||
  HassioResponse,
 | 
			
		||||
  ignoredStatusCodes,
 | 
			
		||||
} from "../../../src/data/hassio/common";
 | 
			
		||||
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
 | 
			
		||||
import {
 | 
			
		||||
@@ -164,8 +165,9 @@ export class HassioUpdate extends LitElement {
 | 
			
		||||
    try {
 | 
			
		||||
      await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      // Only show an error if the status code was not 504, or no status at all (connection terminated)
 | 
			
		||||
      if (err.status_code && err.status_code !== 504) {
 | 
			
		||||
      // Only show an error if the status code was not expected (user behind proxy)
 | 
			
		||||
      // or no status at all(connection terminated)
 | 
			
		||||
      if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
 | 
			
		||||
        showAlertDialog(this, {
 | 
			
		||||
          title: "Update failed",
 | 
			
		||||
          text: extractApiErrorMessage(err),
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,10 @@ import "../../../src/components/buttons/ha-progress-button";
 | 
			
		||||
import "../../../src/components/ha-button-menu";
 | 
			
		||||
import "../../../src/components/ha-card";
 | 
			
		||||
import "../../../src/components/ha-settings-row";
 | 
			
		||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
 | 
			
		||||
import {
 | 
			
		||||
  extractApiErrorMessage,
 | 
			
		||||
  ignoredStatusCodes,
 | 
			
		||||
} from "../../../src/data/hassio/common";
 | 
			
		||||
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
 | 
			
		||||
import {
 | 
			
		||||
  changeHostOptions,
 | 
			
		||||
@@ -245,10 +248,13 @@ class HassioHostInfo extends LitElement {
 | 
			
		||||
    try {
 | 
			
		||||
      await rebootHost(this.hass);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      showAlertDialog(this, {
 | 
			
		||||
        title: "Failed to reboot",
 | 
			
		||||
        text: extractApiErrorMessage(err),
 | 
			
		||||
      });
 | 
			
		||||
      // Ignore connection errors, these are all expected
 | 
			
		||||
      if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
 | 
			
		||||
        showAlertDialog(this, {
 | 
			
		||||
          title: "Failed to reboot",
 | 
			
		||||
          text: extractApiErrorMessage(err),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    button.progress = false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -272,10 +278,13 @@ class HassioHostInfo extends LitElement {
 | 
			
		||||
    try {
 | 
			
		||||
      await shutdownHost(this.hass);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      showAlertDialog(this, {
 | 
			
		||||
        title: "Failed to shutdown",
 | 
			
		||||
        text: extractApiErrorMessage(err),
 | 
			
		||||
      });
 | 
			
		||||
      // Ignore connection errors, these are all expected
 | 
			
		||||
      if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
 | 
			
		||||
        showAlertDialog(this, {
 | 
			
		||||
          title: "Failed to shutdown",
 | 
			
		||||
          text: extractApiErrorMessage(err),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    button.progress = false;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import {
 | 
			
		||||
  setSupervisorOption,
 | 
			
		||||
  SupervisorOptions,
 | 
			
		||||
  updateSupervisor,
 | 
			
		||||
  fetchHassioSupervisorInfo,
 | 
			
		||||
} from "../../../src/data/hassio/supervisor";
 | 
			
		||||
import {
 | 
			
		||||
  showAlertDialog,
 | 
			
		||||
@@ -176,10 +177,11 @@ class HassioSupervisorInfo extends LitElement {
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const data: Partial<SupervisorOptions> = {
 | 
			
		||||
        channel: this.supervisorInfo.channel !== "stable" ? "beta" : "stable",
 | 
			
		||||
        channel: this.supervisorInfo.channel === "stable" ? "beta" : "stable",
 | 
			
		||||
      };
 | 
			
		||||
      await setSupervisorOption(this.hass, data);
 | 
			
		||||
      await reloadSupervisor(this.hass);
 | 
			
		||||
      this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      showAlertDialog(this, {
 | 
			
		||||
        title: "Failed to set supervisor option",
 | 
			
		||||
@@ -195,6 +197,7 @@ class HassioSupervisorInfo extends LitElement {
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await reloadSupervisor(this.hass);
 | 
			
		||||
      this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      showAlertDialog(this, {
 | 
			
		||||
        title: "Failed to reload the supervisor",
 | 
			
		||||
 
 | 
			
		||||
@@ -79,6 +79,7 @@
 | 
			
		||||
    "@polymer/polymer": "3.1.0",
 | 
			
		||||
    "@thomasloven/round-slider": "0.5.0",
 | 
			
		||||
    "@types/chromecast-caf-sender": "^1.0.3",
 | 
			
		||||
    "@types/sortablejs": "^1.10.6",
 | 
			
		||||
    "@vaadin/vaadin-combo-box": "^5.0.10",
 | 
			
		||||
    "@vaadin/vaadin-date-picker": "^4.0.7",
 | 
			
		||||
    "@vue/web-component-wrapper": "^1.2.0",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
									
									
									
									
								
							@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
 | 
			
		||||
 | 
			
		||||
setup(
 | 
			
		||||
    name="home-assistant-frontend",
 | 
			
		||||
    version="20200904.0",
 | 
			
		||||
    version="20200912.0",
 | 
			
		||||
    description="The Home Assistant frontend",
 | 
			
		||||
    url="https://github.com/home-assistant/home-assistant-polymer",
 | 
			
		||||
    author="The Home Assistant Authors",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								src/common/config/components_with_service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/common/config/components_with_service.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
import { HomeAssistant } from "../../types";
 | 
			
		||||
 | 
			
		||||
/** Return an array of domains with the service. */
 | 
			
		||||
export const componentsWithService = (
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  service: string
 | 
			
		||||
): Array<string> =>
 | 
			
		||||
  hass &&
 | 
			
		||||
  Object.keys(hass.services).filter((key) => service in hass.services[key]);
 | 
			
		||||
@@ -44,7 +44,6 @@ export const DOMAINS_WITH_MORE_INFO = [
 | 
			
		||||
  "script",
 | 
			
		||||
  "sun",
 | 
			
		||||
  "timer",
 | 
			
		||||
  "updater",
 | 
			
		||||
  "vacuum",
 | 
			
		||||
  "water_heater",
 | 
			
		||||
  "weather",
 | 
			
		||||
 
 | 
			
		||||
@@ -105,12 +105,12 @@ const processTheme = (
 | 
			
		||||
  const keys = {};
 | 
			
		||||
  for (const key of Object.keys(combinedTheme)) {
 | 
			
		||||
    const prefixedKey = `--${key}`;
 | 
			
		||||
    const value = combinedTheme[key]!;
 | 
			
		||||
    const value = String(combinedTheme[key]!);
 | 
			
		||||
    styles[prefixedKey] = value;
 | 
			
		||||
    keys[prefixedKey] = "";
 | 
			
		||||
 | 
			
		||||
    // Try to create a rgb value for this key if it is not a var
 | 
			
		||||
    if (!value.startsWith("#")) {
 | 
			
		||||
    if (value.startsWith("#")) {
 | 
			
		||||
      // Can't convert non hex value
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,49 +3,51 @@ import { HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
/** Return an icon representing a binary sensor state. */
 | 
			
		||||
 | 
			
		||||
export const binarySensorIcon = (state: HassEntity) => {
 | 
			
		||||
  const activated = state.state && state.state === "off";
 | 
			
		||||
  const is_off = state.state && state.state === "off";
 | 
			
		||||
  switch (state.attributes.device_class) {
 | 
			
		||||
    case "battery":
 | 
			
		||||
      return activated ? "hass:battery" : "hass:battery-outline";
 | 
			
		||||
      return is_off ? "hass:battery" : "hass:battery-outline";
 | 
			
		||||
    case "battery_charging":
 | 
			
		||||
      return is_off ? "hass:battery" : "hass:battery-charging";
 | 
			
		||||
    case "cold":
 | 
			
		||||
      return activated ? "hass:thermometer" : "hass:snowflake";
 | 
			
		||||
      return is_off ? "hass:thermometer" : "hass:snowflake";
 | 
			
		||||
    case "connectivity":
 | 
			
		||||
      return activated ? "hass:server-network-off" : "hass:server-network";
 | 
			
		||||
      return is_off ? "hass:server-network-off" : "hass:server-network";
 | 
			
		||||
    case "door":
 | 
			
		||||
      return activated ? "hass:door-closed" : "hass:door-open";
 | 
			
		||||
      return is_off ? "hass:door-closed" : "hass:door-open";
 | 
			
		||||
    case "garage_door":
 | 
			
		||||
      return activated ? "hass:garage" : "hass:garage-open";
 | 
			
		||||
      return is_off ? "hass:garage" : "hass:garage-open";
 | 
			
		||||
    case "gas":
 | 
			
		||||
    case "power":
 | 
			
		||||
    case "problem":
 | 
			
		||||
    case "safety":
 | 
			
		||||
    case "smoke":
 | 
			
		||||
      return activated ? "hass:shield-check" : "hass:alert";
 | 
			
		||||
      return is_off ? "hass:shield-check" : "hass:alert";
 | 
			
		||||
    case "heat":
 | 
			
		||||
      return activated ? "hass:thermometer" : "hass:fire";
 | 
			
		||||
      return is_off ? "hass:thermometer" : "hass:fire";
 | 
			
		||||
    case "light":
 | 
			
		||||
      return activated ? "hass:brightness-5" : "hass:brightness-7";
 | 
			
		||||
      return is_off ? "hass:brightness-5" : "hass:brightness-7";
 | 
			
		||||
    case "lock":
 | 
			
		||||
      return activated ? "hass:lock" : "hass:lock-open";
 | 
			
		||||
      return is_off ? "hass:lock" : "hass:lock-open";
 | 
			
		||||
    case "moisture":
 | 
			
		||||
      return activated ? "hass:water-off" : "hass:water";
 | 
			
		||||
      return is_off ? "hass:water-off" : "hass:water";
 | 
			
		||||
    case "motion":
 | 
			
		||||
      return activated ? "hass:walk" : "hass:run";
 | 
			
		||||
      return is_off ? "hass:walk" : "hass:run";
 | 
			
		||||
    case "occupancy":
 | 
			
		||||
      return activated ? "hass:home-outline" : "hass:home";
 | 
			
		||||
      return is_off ? "hass:home-outline" : "hass:home";
 | 
			
		||||
    case "opening":
 | 
			
		||||
      return activated ? "hass:square" : "hass:square-outline";
 | 
			
		||||
      return is_off ? "hass:square" : "hass:square-outline";
 | 
			
		||||
    case "plug":
 | 
			
		||||
      return activated ? "hass:power-plug-off" : "hass:power-plug";
 | 
			
		||||
      return is_off ? "hass:power-plug-off" : "hass:power-plug";
 | 
			
		||||
    case "presence":
 | 
			
		||||
      return activated ? "hass:home-outline" : "hass:home";
 | 
			
		||||
      return is_off ? "hass:home-outline" : "hass:home";
 | 
			
		||||
    case "sound":
 | 
			
		||||
      return activated ? "hass:music-note-off" : "hass:music-note";
 | 
			
		||||
      return is_off ? "hass:music-note-off" : "hass:music-note";
 | 
			
		||||
    case "vibration":
 | 
			
		||||
      return activated ? "hass:crop-portrait" : "hass:vibrate";
 | 
			
		||||
      return is_off ? "hass:crop-portrait" : "hass:vibrate";
 | 
			
		||||
    case "window":
 | 
			
		||||
      return activated ? "hass:window-closed" : "hass:window-open";
 | 
			
		||||
      return is_off ? "hass:window-closed" : "hass:window-open";
 | 
			
		||||
    default:
 | 
			
		||||
      return activated ? "hass:radiobox-blank" : "hass:checkbox-marked-circle";
 | 
			
		||||
      return is_off ? "hass:radiobox-blank" : "hass:checkbox-marked-circle";
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,10 @@ import { HomeAssistant } from "../../types";
 | 
			
		||||
import { DOMAINS_WITH_CARD } from "../const";
 | 
			
		||||
import { canToggleState } from "./can_toggle_state";
 | 
			
		||||
import { computeStateDomain } from "./compute_state_domain";
 | 
			
		||||
import { UNAVAILABLE } from "../../data/entity";
 | 
			
		||||
 | 
			
		||||
export const stateCardType = (hass: HomeAssistant, stateObj: HassEntity) => {
 | 
			
		||||
  if (stateObj.state === "unavailable") {
 | 
			
		||||
  if (stateObj.state === UNAVAILABLE) {
 | 
			
		||||
    return "display";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,12 @@
 | 
			
		||||
import { HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
import durationToSeconds from "../datetime/duration_to_seconds";
 | 
			
		||||
 | 
			
		||||
export const timerTimeRemaining = (stateObj: HassEntity) => {
 | 
			
		||||
export const timerTimeRemaining = (
 | 
			
		||||
  stateObj: HassEntity
 | 
			
		||||
): undefined | number => {
 | 
			
		||||
  if (!stateObj.attributes.remaining) {
 | 
			
		||||
    return undefined;
 | 
			
		||||
  }
 | 
			
		||||
  let timeRemaining = durationToSeconds(stateObj.attributes.remaining);
 | 
			
		||||
 | 
			
		||||
  if (stateObj.state === "active") {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										50
									
								
								src/common/util/throttle.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/common/util/throttle.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
// From: underscore.js https://github.com/jashkenas/underscore/blob/master/underscore.js
 | 
			
		||||
 | 
			
		||||
// Returns a function, that, when invoked, will only be triggered at most once
 | 
			
		||||
// during a given window of time. Normally, the throttled function will run
 | 
			
		||||
// as much as it can, without ever going more than once per `wait` duration;
 | 
			
		||||
// but if you'd like to disable the execution on the leading edge, pass
 | 
			
		||||
// `false for leading`. To disable execution on the trailing edge, ditto.
 | 
			
		||||
export const throttle = <T extends Function>(
 | 
			
		||||
  func: T,
 | 
			
		||||
  wait: number,
 | 
			
		||||
  leading = true,
 | 
			
		||||
  trailing = true
 | 
			
		||||
): T => {
 | 
			
		||||
  let timeout: number | undefined;
 | 
			
		||||
  let previous = 0;
 | 
			
		||||
  let context: any;
 | 
			
		||||
  let args: any;
 | 
			
		||||
  const later = () => {
 | 
			
		||||
    previous = leading === false ? 0 : Date.now();
 | 
			
		||||
    timeout = undefined;
 | 
			
		||||
    func.apply(context, args);
 | 
			
		||||
    if (!timeout) {
 | 
			
		||||
      context = null;
 | 
			
		||||
      args = null;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  return function (...argmnts) {
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    // @typescript-eslint/no-this-alias
 | 
			
		||||
    context = this;
 | 
			
		||||
    args = argmnts;
 | 
			
		||||
 | 
			
		||||
    const now = Date.now();
 | 
			
		||||
    if (!previous && leading === false) {
 | 
			
		||||
      previous = now;
 | 
			
		||||
    }
 | 
			
		||||
    const remaining = wait - (now - previous);
 | 
			
		||||
    if (remaining <= 0 || remaining > wait) {
 | 
			
		||||
      if (timeout) {
 | 
			
		||||
        clearTimeout(timeout);
 | 
			
		||||
        timeout = undefined;
 | 
			
		||||
      }
 | 
			
		||||
      previous = now;
 | 
			
		||||
      func.apply(context, args);
 | 
			
		||||
    } else if (!timeout && trailing !== false) {
 | 
			
		||||
      timeout = window.setTimeout(later, remaining);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										178
									
								
								src/components/entity/ha-entity-attribute-picker.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								src/components/entity/ha-entity-attribute-picker.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,178 @@
 | 
			
		||||
import "@polymer/paper-input/paper-input";
 | 
			
		||||
import "@polymer/paper-item/paper-item";
 | 
			
		||||
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
 | 
			
		||||
import { HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
import {
 | 
			
		||||
  css,
 | 
			
		||||
  CSSResult,
 | 
			
		||||
  customElement,
 | 
			
		||||
  html,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  PropertyValues,
 | 
			
		||||
  query,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
import { PolymerChangedEvent } from "../../polymer-types";
 | 
			
		||||
import { HomeAssistant } from "../../types";
 | 
			
		||||
import "../ha-icon-button";
 | 
			
		||||
import "./state-badge";
 | 
			
		||||
 | 
			
		||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
 | 
			
		||||
 | 
			
		||||
const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => {
 | 
			
		||||
  if (!root.firstElementChild) {
 | 
			
		||||
    root.innerHTML = `
 | 
			
		||||
      <style>
 | 
			
		||||
        paper-item {
 | 
			
		||||
          margin: -10px;
 | 
			
		||||
          padding: 0;
 | 
			
		||||
        }
 | 
			
		||||
      </style>
 | 
			
		||||
      <paper-item></paper-item>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
  root.querySelector("paper-item")!.textContent = model.item;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@customElement("ha-entity-attribute-picker")
 | 
			
		||||
class HaEntityAttributePicker extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property() public entityId?: string;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public autofocus = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public disabled = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, attribute: "allow-custom-value" })
 | 
			
		||||
  public allowCustomValue;
 | 
			
		||||
 | 
			
		||||
  @property() public label?: string;
 | 
			
		||||
 | 
			
		||||
  @property() public value?: string;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) private _opened = false;
 | 
			
		||||
 | 
			
		||||
  @query("vaadin-combo-box-light") private _comboBox!: HTMLElement;
 | 
			
		||||
 | 
			
		||||
  protected shouldUpdate(changedProps: PropertyValues) {
 | 
			
		||||
    return !(!changedProps.has("_opened") && this._opened);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected updated(changedProps: PropertyValues) {
 | 
			
		||||
    if (changedProps.has("_opened") && this._opened) {
 | 
			
		||||
      const state = this.entityId ? this.hass.states[this.entityId] : undefined;
 | 
			
		||||
      (this._comboBox as any).items = state
 | 
			
		||||
        ? Object.keys(state.attributes)
 | 
			
		||||
        : [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render(): TemplateResult {
 | 
			
		||||
    if (!this.hass) {
 | 
			
		||||
      return html``;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <vaadin-combo-box-light
 | 
			
		||||
        .value=${this._value}
 | 
			
		||||
        .allowCustomValue=${this.allowCustomValue}
 | 
			
		||||
        .renderer=${rowRenderer}
 | 
			
		||||
        @opened-changed=${this._openedChanged}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
      >
 | 
			
		||||
        <paper-input
 | 
			
		||||
          .autofocus=${this.autofocus}
 | 
			
		||||
          .label=${this.label ??
 | 
			
		||||
          this.hass.localize(
 | 
			
		||||
            "ui.components.entity.entity-attribute-picker.attribute"
 | 
			
		||||
          )}
 | 
			
		||||
          .value=${this._value}
 | 
			
		||||
          .disabled=${this.disabled || !this.entityId}
 | 
			
		||||
          class="input"
 | 
			
		||||
          autocapitalize="none"
 | 
			
		||||
          autocomplete="off"
 | 
			
		||||
          autocorrect="off"
 | 
			
		||||
          spellcheck="false"
 | 
			
		||||
        >
 | 
			
		||||
          ${this.value
 | 
			
		||||
            ? html`
 | 
			
		||||
                <ha-icon-button
 | 
			
		||||
                  aria-label=${this.hass.localize(
 | 
			
		||||
                    "ui.components.entity.entity-picker.clear"
 | 
			
		||||
                  )}
 | 
			
		||||
                  slot="suffix"
 | 
			
		||||
                  class="clear-button"
 | 
			
		||||
                  icon="hass:close"
 | 
			
		||||
                  @click=${this._clearValue}
 | 
			
		||||
                  no-ripple
 | 
			
		||||
                >
 | 
			
		||||
                  Clear
 | 
			
		||||
                </ha-icon-button>
 | 
			
		||||
              `
 | 
			
		||||
            : ""}
 | 
			
		||||
 | 
			
		||||
          <ha-icon-button
 | 
			
		||||
            aria-label=${this.hass.localize(
 | 
			
		||||
              "ui.components.entity.entity-attribute-picker.show_attributes"
 | 
			
		||||
            )}
 | 
			
		||||
            slot="suffix"
 | 
			
		||||
            class="toggle-button"
 | 
			
		||||
            .icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
 | 
			
		||||
          >
 | 
			
		||||
            Toggle
 | 
			
		||||
          </ha-icon-button>
 | 
			
		||||
        </paper-input>
 | 
			
		||||
      </vaadin-combo-box-light>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _clearValue(ev: Event) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    this._setValue("");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private get _value() {
 | 
			
		||||
    return this.value || "";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _openedChanged(ev: PolymerChangedEvent<boolean>) {
 | 
			
		||||
    this._opened = ev.detail.value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _valueChanged(ev: PolymerChangedEvent<string>) {
 | 
			
		||||
    const newValue = ev.detail.value;
 | 
			
		||||
    if (newValue !== this._value) {
 | 
			
		||||
      this._setValue(newValue);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _setValue(value: string) {
 | 
			
		||||
    this.value = value;
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      fireEvent(this, "value-changed", { value });
 | 
			
		||||
      fireEvent(this, "change");
 | 
			
		||||
    }, 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get styles(): CSSResult {
 | 
			
		||||
    return css`
 | 
			
		||||
      paper-input > ha-icon-button {
 | 
			
		||||
        --mdc-icon-button-size: 24px;
 | 
			
		||||
        padding: 0px 2px;
 | 
			
		||||
        color: var(--secondary-text-color);
 | 
			
		||||
      }
 | 
			
		||||
      [hidden] {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-entity-attribute-picker": HaEntityAttributePicker;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import "../ha-icon-button";
 | 
			
		||||
import "@polymer/paper-input/paper-input";
 | 
			
		||||
import "@polymer/paper-item/paper-icon-item";
 | 
			
		||||
import "@polymer/paper-item/paper-item-body";
 | 
			
		||||
@@ -7,6 +6,7 @@ import { HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
import {
 | 
			
		||||
  css,
 | 
			
		||||
  CSSResult,
 | 
			
		||||
  customElement,
 | 
			
		||||
  html,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
@@ -20,6 +20,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
 | 
			
		||||
import { computeStateName } from "../../common/entity/compute_state_name";
 | 
			
		||||
import { PolymerChangedEvent } from "../../polymer-types";
 | 
			
		||||
import { HomeAssistant } from "../../types";
 | 
			
		||||
import "../ha-icon-button";
 | 
			
		||||
import "./state-badge";
 | 
			
		||||
 | 
			
		||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
 | 
			
		||||
@@ -51,7 +52,8 @@ const rowRenderer = (
 | 
			
		||||
  root.querySelector("[secondary]")!.textContent = model.item.entity_id;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class HaEntityPicker extends LitElement {
 | 
			
		||||
@customElement("ha-entity-picker")
 | 
			
		||||
export class HaEntityPicker extends LitElement {
 | 
			
		||||
  @property({ type: Boolean }) public autofocus = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public disabled?: boolean;
 | 
			
		||||
@@ -95,6 +97,8 @@ class HaEntityPicker extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @query("vaadin-combo-box-light") private _comboBox!: HTMLElement;
 | 
			
		||||
 | 
			
		||||
  private _initedStates = false;
 | 
			
		||||
 | 
			
		||||
  private _getStates = memoizeOne(
 | 
			
		||||
    (
 | 
			
		||||
      _opened: boolean,
 | 
			
		||||
@@ -148,11 +152,18 @@ class HaEntityPicker extends LitElement {
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  protected shouldUpdate(changedProps: PropertyValues) {
 | 
			
		||||
    if (
 | 
			
		||||
      changedProps.has("value") ||
 | 
			
		||||
      changedProps.has("label") ||
 | 
			
		||||
      changedProps.has("disabled")
 | 
			
		||||
    ) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    return !(!changedProps.has("_opened") && this._opened);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected updated(changedProps: PropertyValues) {
 | 
			
		||||
    if (changedProps.has("_opened") && this._opened) {
 | 
			
		||||
    if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
 | 
			
		||||
      const states = this._getStates(
 | 
			
		||||
        this._opened,
 | 
			
		||||
        this.hass,
 | 
			
		||||
@@ -162,6 +173,7 @@ class HaEntityPicker extends LitElement {
 | 
			
		||||
        this.includeDeviceClasses
 | 
			
		||||
      );
 | 
			
		||||
      (this._comboBox as any).items = states;
 | 
			
		||||
      this._initedStates = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -169,7 +181,6 @@ class HaEntityPicker extends LitElement {
 | 
			
		||||
    if (!this.hass) {
 | 
			
		||||
      return html``;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <vaadin-combo-box-light
 | 
			
		||||
        item-value-path="entity_id"
 | 
			
		||||
@@ -267,8 +278,6 @@ class HaEntityPicker extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define("ha-entity-picker", HaEntityPicker);
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-entity-picker": HaEntityPicker;
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ import { stateIcon } from "../../common/entity/state_icon";
 | 
			
		||||
import { timerTimeRemaining } from "../../common/entity/timer_time_remaining";
 | 
			
		||||
import { HomeAssistant } from "../../types";
 | 
			
		||||
import "../ha-label-badge";
 | 
			
		||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-state-label-badge")
 | 
			
		||||
export class HaStateLabelBadge extends LitElement {
 | 
			
		||||
@@ -81,7 +82,8 @@ export class HaStateLabelBadge extends LitElement {
 | 
			
		||||
          ? ""
 | 
			
		||||
          : this.image
 | 
			
		||||
          ? this.image
 | 
			
		||||
          : state.attributes.entity_picture_local || state.attributes.entity_picture}"
 | 
			
		||||
          : state.attributes.entity_picture_local ||
 | 
			
		||||
            state.attributes.entity_picture}"
 | 
			
		||||
        .label="${this._computeLabel(domain, state, this._timerTimeRemaining)}"
 | 
			
		||||
        .description="${this.name ? this.name : computeStateName(state)}"
 | 
			
		||||
      ></ha-label-badge>
 | 
			
		||||
@@ -108,7 +110,7 @@ export class HaStateLabelBadge extends LitElement {
 | 
			
		||||
        return null;
 | 
			
		||||
      case "sensor":
 | 
			
		||||
      default:
 | 
			
		||||
        return state.state === "unknown"
 | 
			
		||||
        return state.state === UNKNOWN
 | 
			
		||||
          ? "-"
 | 
			
		||||
          : state.attributes.unit_of_measurement
 | 
			
		||||
          ? state.state
 | 
			
		||||
@@ -121,7 +123,7 @@ export class HaStateLabelBadge extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _computeIcon(domain: string, state: HassEntity) {
 | 
			
		||||
    if (state.state === "unavailable") {
 | 
			
		||||
    if (state.state === UNAVAILABLE) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    switch (domain) {
 | 
			
		||||
@@ -166,7 +168,7 @@ export class HaStateLabelBadge extends LitElement {
 | 
			
		||||
 | 
			
		||||
  private _computeLabel(domain, state, _timerTimeRemaining) {
 | 
			
		||||
    if (
 | 
			
		||||
      state.state === "unavailable" ||
 | 
			
		||||
      state.state === UNAVAILABLE ||
 | 
			
		||||
      ["device_tracker", "alarm_control_panel", "person"].includes(domain)
 | 
			
		||||
    ) {
 | 
			
		||||
      // Localize the state with a special state_badge namespace, which has variations of
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,11 @@ class HaCameraStream extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: false }) public stateObj?: CameraEntity;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public showControls = false;
 | 
			
		||||
  @property({ type: Boolean, attribute: "controls" })
 | 
			
		||||
  public controls = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, attribute: "muted" })
 | 
			
		||||
  public muted = false;
 | 
			
		||||
 | 
			
		||||
  // We keep track if we should force MJPEG with a string
 | 
			
		||||
  // that way it automatically resets if we change entity.
 | 
			
		||||
@@ -35,7 +39,7 @@ class HaCameraStream extends LitElement {
 | 
			
		||||
  @internalProperty() private _url?: string;
 | 
			
		||||
 | 
			
		||||
  protected render(): TemplateResult {
 | 
			
		||||
    if (!this.stateObj || (!this._forceMJPEG && !this._url)) {
 | 
			
		||||
    if (!this.stateObj) {
 | 
			
		||||
      return html``;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -52,21 +56,23 @@ class HaCameraStream extends LitElement {
 | 
			
		||||
              )} camera.`}
 | 
			
		||||
            />
 | 
			
		||||
          `
 | 
			
		||||
        : html`
 | 
			
		||||
        : this._url
 | 
			
		||||
        ? html`
 | 
			
		||||
            <ha-hls-player
 | 
			
		||||
              autoplay
 | 
			
		||||
              muted
 | 
			
		||||
              playsinline
 | 
			
		||||
              ?controls=${this.showControls}
 | 
			
		||||
              .muted=${this.muted}
 | 
			
		||||
              .controls=${this.controls}
 | 
			
		||||
              .hass=${this.hass}
 | 
			
		||||
              .url=${this._url!}
 | 
			
		||||
              .url=${this._url}
 | 
			
		||||
            ></ha-hls-player>
 | 
			
		||||
          `}
 | 
			
		||||
          `
 | 
			
		||||
        : ""}
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected updated(changedProps: PropertyValues): void {
 | 
			
		||||
    if (changedProps.has("stateObj")) {
 | 
			
		||||
    if (changedProps.has("stateObj") && !this._shouldRenderMJPEG) {
 | 
			
		||||
      this._forceMJPEG = undefined;
 | 
			
		||||
      this._getStreamUrl();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -97,6 +97,7 @@ export class HaCodeEditor extends UpdatingElement {
 | 
			
		||||
      .CodeMirror {
 | 
			
		||||
        height: var(--code-mirror-height, auto);
 | 
			
		||||
        direction: var(--code-mirror-direction, ltr);
 | 
			
		||||
        font-family: var(--code-font-family, monospace);
 | 
			
		||||
      }
 | 
			
		||||
      .CodeMirror-scroll {
 | 
			
		||||
        max-height: var(--code-mirror-max-height, --code-mirror-height);
 | 
			
		||||
 
 | 
			
		||||
@@ -176,6 +176,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
 | 
			
		||||
    this.drawColorWheel();
 | 
			
		||||
    this.drawMarker();
 | 
			
		||||
 | 
			
		||||
    if (this.desiredHsColor) {
 | 
			
		||||
      this.setMarkerOnColor(this.desiredHsColor);
 | 
			
		||||
      this.applyColorToCanvas(this.desiredHsColor);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.interactionLayer.addEventListener("mousedown", (ev) =>
 | 
			
		||||
      this.onMouseDown(ev)
 | 
			
		||||
    );
 | 
			
		||||
@@ -319,6 +324,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
 | 
			
		||||
 | 
			
		||||
  // set marker position to the given color
 | 
			
		||||
  setMarkerOnColor(hs) {
 | 
			
		||||
    if (!this.marker || !this.tooltip) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const dist = hs.s * this.radius;
 | 
			
		||||
    const theta = ((hs.h - 180) / 180) * Math.PI;
 | 
			
		||||
    const markerdX = -dist * Math.cos(theta);
 | 
			
		||||
@@ -330,6 +338,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
 | 
			
		||||
 | 
			
		||||
  // apply given color to interface elements
 | 
			
		||||
  applyColorToCanvas(hs) {
 | 
			
		||||
    if (!this.interactionLayer) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    // we're not really converting hs to hsl here, but we keep it cheap
 | 
			
		||||
    // setting the color on the interactionLayer, the svg elements can inherit
 | 
			
		||||
    this.interactionLayer.style.color = `hsl(${hs.h}, 100%, ${
 | 
			
		||||
 
 | 
			
		||||
@@ -64,6 +64,7 @@ export class HaDialog extends MwcDialog {
 | 
			
		||||
        }
 | 
			
		||||
        .mdc-dialog .mdc-dialog__surface {
 | 
			
		||||
          position: var(--dialog-surface-position, relative);
 | 
			
		||||
          top: var(--dialog-surface-top);
 | 
			
		||||
          min-height: var(--mdc-dialog-min-height, auto);
 | 
			
		||||
        }
 | 
			
		||||
        :host([flexContent]) .mdc-dialog .mdc-dialog__content {
 | 
			
		||||
 
 | 
			
		||||
@@ -61,7 +61,7 @@ class HaHLSPlayer extends LitElement {
 | 
			
		||||
    return html`
 | 
			
		||||
      <video
 | 
			
		||||
        ?autoplay=${this.autoPlay}
 | 
			
		||||
        ?muted=${this.muted}
 | 
			
		||||
        .muted=${this.muted}
 | 
			
		||||
        ?playsinline=${this.playsInline}
 | 
			
		||||
        ?controls=${this.controls}
 | 
			
		||||
        @loadeddata=${this._elementResized}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import "@polymer/paper-item/paper-item-body";
 | 
			
		||||
import {
 | 
			
		||||
  css,
 | 
			
		||||
  CSSResult,
 | 
			
		||||
@@ -7,7 +8,6 @@ import {
 | 
			
		||||
  property,
 | 
			
		||||
  SVGTemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import "@polymer/paper-item/paper-item-body";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-settings-row")
 | 
			
		||||
export class HaSettingsRow extends LitElement {
 | 
			
		||||
@@ -49,6 +49,9 @@ export class HaSettingsRow extends LitElement {
 | 
			
		||||
        border-top: 1px solid var(--divider-color);
 | 
			
		||||
        padding-bottom: 8px;
 | 
			
		||||
      }
 | 
			
		||||
      ::slotted(ha-switch) {
 | 
			
		||||
        padding: 16px 0;
 | 
			
		||||
      }
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,77 +0,0 @@
 | 
			
		||||
import { html } from "lit-element";
 | 
			
		||||
 | 
			
		||||
export const sortStyles = html`
 | 
			
		||||
  <style>
 | 
			
		||||
    #sortable a:nth-of-type(2n) paper-icon-item {
 | 
			
		||||
      animation-name: keyframes1;
 | 
			
		||||
      animation-iteration-count: infinite;
 | 
			
		||||
      transform-origin: 50% 10%;
 | 
			
		||||
      animation-delay: -0.75s;
 | 
			
		||||
      animation-duration: 0.25s;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #sortable a:nth-of-type(2n-1) paper-icon-item {
 | 
			
		||||
      animation-name: keyframes2;
 | 
			
		||||
      animation-iteration-count: infinite;
 | 
			
		||||
      animation-direction: alternate;
 | 
			
		||||
      transform-origin: 30% 5%;
 | 
			
		||||
      animation-delay: -0.5s;
 | 
			
		||||
      animation-duration: 0.33s;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #sortable {
 | 
			
		||||
      outline: none;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .sortable-ghost {
 | 
			
		||||
      opacity: 0.4;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .sortable-fallback {
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @keyframes keyframes1 {
 | 
			
		||||
      0% {
 | 
			
		||||
        transform: rotate(-1deg);
 | 
			
		||||
        animation-timing-function: ease-in;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      50% {
 | 
			
		||||
        transform: rotate(1.5deg);
 | 
			
		||||
        animation-timing-function: ease-out;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @keyframes keyframes2 {
 | 
			
		||||
      0% {
 | 
			
		||||
        transform: rotate(1deg);
 | 
			
		||||
        animation-timing-function: ease-in;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      50% {
 | 
			
		||||
        transform: rotate(-1.5deg);
 | 
			
		||||
        animation-timing-function: ease-out;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .hide-panel {
 | 
			
		||||
      display: none;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      right: 8px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    :host([expanded]) .hide-panel {
 | 
			
		||||
      display: inline-flex;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    paper-icon-item.hidden-panel,
 | 
			
		||||
    paper-icon-item.hidden-panel span,
 | 
			
		||||
    paper-icon-item.hidden-panel ha-icon[slot="item-icon"] {
 | 
			
		||||
      color: var(--secondary-text-color);
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
    }
 | 
			
		||||
  </style>
 | 
			
		||||
`;
 | 
			
		||||
@@ -23,7 +23,6 @@ import {
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  PropertyValues,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { classMap } from "lit-html/directives/class-map";
 | 
			
		||||
import { guard } from "lit-html/directives/guard";
 | 
			
		||||
@@ -160,13 +159,13 @@ const computePanels = memoizeOne(
 | 
			
		||||
 | 
			
		||||
let Sortable;
 | 
			
		||||
 | 
			
		||||
let sortStyles: TemplateResult;
 | 
			
		||||
let sortStyles: CSSResult;
 | 
			
		||||
 | 
			
		||||
@customElement("ha-sidebar")
 | 
			
		||||
class HaSidebar extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property() public narrow!: boolean;
 | 
			
		||||
  @property({ type: Boolean, reflect: true }) public narrow!: boolean;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public alwaysExpand = false;
 | 
			
		||||
 | 
			
		||||
@@ -228,8 +227,21 @@ class HaSidebar extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      ${this._editMode ? sortStyles : ""}
 | 
			
		||||
      <div class="menu">
 | 
			
		||||
      ${this._editMode
 | 
			
		||||
        ? html`
 | 
			
		||||
            <style>
 | 
			
		||||
              ${sortStyles?.cssText}
 | 
			
		||||
            </style>
 | 
			
		||||
          `
 | 
			
		||||
        : ""}
 | 
			
		||||
      <div
 | 
			
		||||
        class="menu"
 | 
			
		||||
        @action=${this._handleAction}
 | 
			
		||||
        .actionHandler=${actionHandler({
 | 
			
		||||
          hasHold: !this._editMode,
 | 
			
		||||
          disabled: this._editMode,
 | 
			
		||||
        })}
 | 
			
		||||
      >
 | 
			
		||||
        ${!this.narrow
 | 
			
		||||
          ? html`
 | 
			
		||||
              <mwc-icon-button
 | 
			
		||||
@@ -247,7 +259,7 @@ class HaSidebar extends LitElement {
 | 
			
		||||
        <div class="title">
 | 
			
		||||
          ${this._editMode
 | 
			
		||||
            ? html`<mwc-button outlined @click=${this._closeEditMode}>
 | 
			
		||||
                DONE
 | 
			
		||||
                ${hass.localize("ui.sidebar.done")}
 | 
			
		||||
              </mwc-button>`
 | 
			
		||||
            : "Home Assistant"}
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -260,11 +272,6 @@ class HaSidebar extends LitElement {
 | 
			
		||||
        @focusout=${this._listboxFocusOut}
 | 
			
		||||
        @scroll=${this._listboxScroll}
 | 
			
		||||
        @keydown=${this._listboxKeydown}
 | 
			
		||||
        @action=${this._handleAction}
 | 
			
		||||
        .actionHandler=${actionHandler({
 | 
			
		||||
          hasHold: !this._editMode,
 | 
			
		||||
          disabled: this._editMode,
 | 
			
		||||
        })}
 | 
			
		||||
      >
 | 
			
		||||
        ${this._editMode
 | 
			
		||||
          ? html`<div id="sortable">
 | 
			
		||||
@@ -280,27 +287,29 @@ class HaSidebar extends LitElement {
 | 
			
		||||
          ? html`
 | 
			
		||||
              ${this._hiddenPanels.map((url) => {
 | 
			
		||||
                const panel = this.hass.panels[url];
 | 
			
		||||
                if (!panel) {
 | 
			
		||||
                  return "";
 | 
			
		||||
                }
 | 
			
		||||
                return html`<paper-icon-item
 | 
			
		||||
                  @click=${this._unhidePanel}
 | 
			
		||||
                  class="hidden-panel"
 | 
			
		||||
                  .panel=${url}
 | 
			
		||||
                >
 | 
			
		||||
                  <ha-icon
 | 
			
		||||
                    slot="item-icon"
 | 
			
		||||
                    .icon=${panel.url_path === "lovelace"
 | 
			
		||||
                    .icon=${panel.url_path === this.hass.defaultPanel
 | 
			
		||||
                      ? "mdi:view-dashboard"
 | 
			
		||||
                      : panel.icon}
 | 
			
		||||
                  ></ha-icon>
 | 
			
		||||
                  <span class="item-text"
 | 
			
		||||
                    >${panel.url_path === "lovelace"
 | 
			
		||||
                    >${panel.url_path === this.hass.defaultPanel
 | 
			
		||||
                      ? hass.localize("panel.states")
 | 
			
		||||
                      : hass.localize(`panel.${panel.title}`) ||
 | 
			
		||||
                        panel.title}</span
 | 
			
		||||
                  >
 | 
			
		||||
                  <ha-svg-icon
 | 
			
		||||
                    class="hide-panel"
 | 
			
		||||
                    .panel=${url}
 | 
			
		||||
                    .path=${mdiPlus}
 | 
			
		||||
                  ></ha-svg-icon>
 | 
			
		||||
                  <mwc-icon-button class="hide-panel">
 | 
			
		||||
                    <ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
 | 
			
		||||
                  </mwc-icon-button>
 | 
			
		||||
                </paper-icon-item>`;
 | 
			
		||||
              })}
 | 
			
		||||
              <div class="spacer" disabled></div>
 | 
			
		||||
@@ -440,6 +449,9 @@ class HaSidebar extends LitElement {
 | 
			
		||||
    subscribeNotifications(this.hass.connection, (notifications) => {
 | 
			
		||||
      this._notifications = notifications;
 | 
			
		||||
    });
 | 
			
		||||
    window.addEventListener("hass-edit-sidebar", () =>
 | 
			
		||||
      this._activateEditMode()
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected updated(changedProps) {
 | 
			
		||||
@@ -472,18 +484,22 @@ class HaSidebar extends LitElement {
 | 
			
		||||
    return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _handleAction(ev: CustomEvent<ActionHandlerDetail>) {
 | 
			
		||||
  private _handleAction(ev: CustomEvent<ActionHandlerDetail>) {
 | 
			
		||||
    if (ev.detail.action !== "hold") {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this._activateEditMode();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _activateEditMode() {
 | 
			
		||||
    if (!Sortable) {
 | 
			
		||||
      const [sortableImport, sortStylesImport] = await Promise.all([
 | 
			
		||||
        import("sortablejs/modular/sortable.core.esm"),
 | 
			
		||||
        import("./ha-sidebar-sort-styles"),
 | 
			
		||||
        import("../resources/ha-sortable-style"),
 | 
			
		||||
      ]);
 | 
			
		||||
 | 
			
		||||
      sortStyles = sortStylesImport.sortStyles;
 | 
			
		||||
      sortStyles = sortStylesImport.sortableStyles;
 | 
			
		||||
 | 
			
		||||
      Sortable = sortableImport.Sortable;
 | 
			
		||||
      Sortable.mount(sortableImport.OnSpill);
 | 
			
		||||
@@ -491,6 +507,8 @@ class HaSidebar extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
    this._editMode = true;
 | 
			
		||||
 | 
			
		||||
    fireEvent(this, "hass-open-menu");
 | 
			
		||||
 | 
			
		||||
    await this.updateComplete;
 | 
			
		||||
 | 
			
		||||
    this._createSortable();
 | 
			
		||||
@@ -515,7 +533,7 @@ class HaSidebar extends LitElement {
 | 
			
		||||
 | 
			
		||||
  private async _hidePanel(ev: Event) {
 | 
			
		||||
    ev.preventDefault();
 | 
			
		||||
    const panel = (ev.target as any).panel;
 | 
			
		||||
    const panel = (ev.currentTarget as any).panel;
 | 
			
		||||
    if (this._hiddenPanels.includes(panel)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -528,7 +546,7 @@ class HaSidebar extends LitElement {
 | 
			
		||||
 | 
			
		||||
  private async _unhidePanel(ev: Event) {
 | 
			
		||||
    ev.preventDefault();
 | 
			
		||||
    const index = this._hiddenPanels.indexOf((ev.target as any).panel);
 | 
			
		||||
    const index = this._hiddenPanels.indexOf((ev.currentTarget as any).panel);
 | 
			
		||||
    if (index < 0) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -640,11 +658,13 @@ class HaSidebar extends LitElement {
 | 
			
		||||
    return panels.map((panel) =>
 | 
			
		||||
      this._renderPanel(
 | 
			
		||||
        panel.url_path,
 | 
			
		||||
        panel.url_path === "lovelace"
 | 
			
		||||
          ? this.hass.localize("panel.states")
 | 
			
		||||
        panel.url_path === this.hass.defaultPanel
 | 
			
		||||
          ? panel.title || this.hass.localize("panel.states")
 | 
			
		||||
          : this.hass.localize(`panel.${panel.title}`) || panel.title,
 | 
			
		||||
        panel.url_path === "lovelace" ? undefined : panel.icon,
 | 
			
		||||
        panel.url_path === "lovelace" ? mdiViewDashboard : undefined
 | 
			
		||||
        panel.icon,
 | 
			
		||||
        panel.url_path === this.hass.defaultPanel && !panel.icon
 | 
			
		||||
          ? mdiViewDashboard
 | 
			
		||||
          : undefined
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
@@ -658,8 +678,8 @@ class HaSidebar extends LitElement {
 | 
			
		||||
    return html`
 | 
			
		||||
      <a
 | 
			
		||||
        aria-role="option"
 | 
			
		||||
        href="${`/${urlPath}`}"
 | 
			
		||||
        data-panel="${urlPath}"
 | 
			
		||||
        href=${`/${urlPath}`}
 | 
			
		||||
        data-panel=${urlPath}
 | 
			
		||||
        tabindex="-1"
 | 
			
		||||
        @mouseenter=${this._itemMouseEnter}
 | 
			
		||||
        @mouseleave=${this._itemMouseLeave}
 | 
			
		||||
@@ -673,12 +693,13 @@ class HaSidebar extends LitElement {
 | 
			
		||||
            : html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
 | 
			
		||||
          <span class="item-text">${title}</span>
 | 
			
		||||
          ${this._editMode
 | 
			
		||||
            ? html`<ha-svg-icon
 | 
			
		||||
            ? html`<mwc-icon-button
 | 
			
		||||
                class="hide-panel"
 | 
			
		||||
                .panel=${urlPath}
 | 
			
		||||
                @click=${this._hidePanel}
 | 
			
		||||
                .path=${mdiClose}
 | 
			
		||||
              ></ha-svg-icon>`
 | 
			
		||||
              >
 | 
			
		||||
                <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
 | 
			
		||||
              </mwc-icon-button>`
 | 
			
		||||
            : ""}
 | 
			
		||||
        </paper-icon-item>
 | 
			
		||||
      </a>
 | 
			
		||||
@@ -747,6 +768,9 @@ class HaSidebar extends LitElement {
 | 
			
		||||
          width: 100%;
 | 
			
		||||
          display: none;
 | 
			
		||||
        }
 | 
			
		||||
        :host([narrow]) .title {
 | 
			
		||||
          padding: 0 16px;
 | 
			
		||||
        }
 | 
			
		||||
        :host([expanded]) .title {
 | 
			
		||||
          display: initial;
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -279,6 +279,7 @@ class LocationEditor extends LitElement {
 | 
			
		||||
      }
 | 
			
		||||
      #map {
 | 
			
		||||
        height: 100%;
 | 
			
		||||
        background: inherit;
 | 
			
		||||
      }
 | 
			
		||||
      .leaflet-edit-move {
 | 
			
		||||
        border-radius: 50%;
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,7 @@ class DialogMediaPlayerBrowse extends LitElement {
 | 
			
		||||
 | 
			
		||||
  public closeDialog() {
 | 
			
		||||
    this._params = undefined;
 | 
			
		||||
    fireEvent(this, "dialog-closed", {dialog: this.localName});
 | 
			
		||||
    fireEvent(this, "dialog-closed", { dialog: this.localName });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render(): TemplateResult {
 | 
			
		||||
@@ -93,6 +93,9 @@ class DialogMediaPlayerBrowse extends LitElement {
 | 
			
		||||
        @media (min-width: 800px) {
 | 
			
		||||
          ha-dialog {
 | 
			
		||||
            --mdc-dialog-max-width: 800px;
 | 
			
		||||
            --dialog-surface-position: fixed;
 | 
			
		||||
            --dialog-surface-top: 40px;
 | 
			
		||||
            --mdc-dialog-max-height: calc(100% - 72px);
 | 
			
		||||
          }
 | 
			
		||||
          ha-media-player-browse {
 | 
			
		||||
            width: 700px;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import "@material/mwc-button/mwc-button";
 | 
			
		||||
import "@material/mwc-fab/mwc-fab";
 | 
			
		||||
import "@material/mwc-list/mwc-list";
 | 
			
		||||
import "@material/mwc-list/mwc-list-item";
 | 
			
		||||
import { mdiArrowLeft, mdiClose, mdiFolder, mdiPlay, mdiPlus } from "@mdi/js";
 | 
			
		||||
import { mdiArrowLeft, mdiClose, mdiPlay, mdiPlus } from "@mdi/js";
 | 
			
		||||
import "@polymer/paper-item/paper-item";
 | 
			
		||||
import "@polymer/paper-listbox/paper-listbox";
 | 
			
		||||
import {
 | 
			
		||||
@@ -18,7 +18,7 @@ import {
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { classMap } from "lit-html/directives/class-map";
 | 
			
		||||
import { ifDefined } from "lit-html/directives/if-defined";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { styleMap } from "lit-html/directives/style-map";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
 | 
			
		||||
import { debounce } from "../../common/util/debounce";
 | 
			
		||||
@@ -26,10 +26,12 @@ import {
 | 
			
		||||
  browseLocalMediaPlayer,
 | 
			
		||||
  browseMediaPlayer,
 | 
			
		||||
  BROWSER_SOURCE,
 | 
			
		||||
  MediaClassBrowserSettings,
 | 
			
		||||
  MediaPickedEvent,
 | 
			
		||||
  MediaPlayerBrowseAction,
 | 
			
		||||
} from "../../data/media-player";
 | 
			
		||||
import type { MediaPlayerItem } from "../../data/media-player";
 | 
			
		||||
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";
 | 
			
		||||
@@ -65,6 +67,8 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _loading = false;
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _error?: { message: string; code: string };
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _mediaPlayerItems: MediaPlayerItem[] = [];
 | 
			
		||||
 | 
			
		||||
  private _resizeObserver?: ResizeObserver;
 | 
			
		||||
@@ -94,6 +98,22 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
      return html`<ha-circular-progress active></ha-circular-progress>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this._error && !this._mediaPlayerItems.length) {
 | 
			
		||||
      if (this.dialog) {
 | 
			
		||||
        this._closeDialogAction();
 | 
			
		||||
        showAlertDialog(this, {
 | 
			
		||||
          title: this.hass.localize(
 | 
			
		||||
            "ui.components.media-browser.media_browsing_error"
 | 
			
		||||
          ),
 | 
			
		||||
          text: this._renderError(this._error),
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        return html`<div class="container">
 | 
			
		||||
          ${this._renderError(this._error)}
 | 
			
		||||
        </div>`;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this._mediaPlayerItems.length) {
 | 
			
		||||
      return html``;
 | 
			
		||||
    }
 | 
			
		||||
@@ -107,129 +127,152 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
        ? this._mediaPlayerItems[this._mediaPlayerItems.length - 2]
 | 
			
		||||
        : undefined;
 | 
			
		||||
 | 
			
		||||
    const hasExpandableChildren:
 | 
			
		||||
      | MediaPlayerItem
 | 
			
		||||
      | undefined = this._hasExpandableChildren(currentItem.children);
 | 
			
		||||
 | 
			
		||||
    const showImages: boolean | undefined = currentItem.children?.some(
 | 
			
		||||
      (child) => child.thumbnail && child.thumbnail !== currentItem.thumbnail
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const mediaType = this.hass.localize(
 | 
			
		||||
      `ui.components.media-browser.content-type.${currentItem.media_content_type}`
 | 
			
		||||
    const subtitle = this.hass.localize(
 | 
			
		||||
      `ui.components.media-browser.class.${currentItem.media_class}`
 | 
			
		||||
    );
 | 
			
		||||
    const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
 | 
			
		||||
    const childrenMediaClass =
 | 
			
		||||
      MediaClassBrowserSettings[currentItem.children_media_class];
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <div
 | 
			
		||||
        class="header  ${classMap({
 | 
			
		||||
          "no-img": !currentItem.thumbnail,
 | 
			
		||||
          "no-dialog": !this.dialog,
 | 
			
		||||
        })}"
 | 
			
		||||
      >
 | 
			
		||||
        <div class="header-content">
 | 
			
		||||
          ${currentItem.thumbnail
 | 
			
		||||
            ? html`
 | 
			
		||||
                <div
 | 
			
		||||
                  class="img"
 | 
			
		||||
                  style="background-image: url(${currentItem.thumbnail})"
 | 
			
		||||
                >
 | 
			
		||||
                  ${this._narrow && currentItem?.can_play
 | 
			
		||||
                    ? html`
 | 
			
		||||
                        <mwc-fab
 | 
			
		||||
                          mini
 | 
			
		||||
                          .item=${currentItem}
 | 
			
		||||
                          @click=${this._actionClicked}
 | 
			
		||||
                        >
 | 
			
		||||
                          <ha-svg-icon
 | 
			
		||||
                            slot="icon"
 | 
			
		||||
                            .label=${this.hass.localize(
 | 
			
		||||
                              `ui.components.media-browser.${this.action}-media`
 | 
			
		||||
        <div class="header-wrapper">
 | 
			
		||||
          <div class="header-content">
 | 
			
		||||
            ${currentItem.thumbnail
 | 
			
		||||
              ? html`
 | 
			
		||||
                  <div
 | 
			
		||||
                    class="img"
 | 
			
		||||
                    style=${styleMap({
 | 
			
		||||
                      backgroundImage: currentItem.thumbnail
 | 
			
		||||
                        ? `url(${currentItem.thumbnail})`
 | 
			
		||||
                        : "none",
 | 
			
		||||
                    })}
 | 
			
		||||
                  >
 | 
			
		||||
                    ${this._narrow && currentItem?.can_play
 | 
			
		||||
                      ? html`
 | 
			
		||||
                          <mwc-fab
 | 
			
		||||
                            mini
 | 
			
		||||
                            .item=${currentItem}
 | 
			
		||||
                            @click=${this._actionClicked}
 | 
			
		||||
                          >
 | 
			
		||||
                            <ha-svg-icon
 | 
			
		||||
                              slot="icon"
 | 
			
		||||
                              .label=${this.hass.localize(
 | 
			
		||||
                                `ui.components.media-browser.${this.action}-media`
 | 
			
		||||
                              )}
 | 
			
		||||
                              .path=${this.action === "play"
 | 
			
		||||
                                ? mdiPlay
 | 
			
		||||
                                : mdiPlus}
 | 
			
		||||
                            ></ha-svg-icon>
 | 
			
		||||
                            ${this.hass.localize(
 | 
			
		||||
                              `ui.components.media-browser.${this.action}`
 | 
			
		||||
                            )}
 | 
			
		||||
                            .path=${this.action === "play" ? mdiPlay : mdiPlus}
 | 
			
		||||
                          ></ha-svg-icon>
 | 
			
		||||
                          ${this.hass.localize(
 | 
			
		||||
                            `ui.components.media-browser.${this.action}`
 | 
			
		||||
                          )}
 | 
			
		||||
                        </mwc-fab>
 | 
			
		||||
                      `
 | 
			
		||||
                    : ""}
 | 
			
		||||
                </div>
 | 
			
		||||
              `
 | 
			
		||||
            : html``}
 | 
			
		||||
          <div class="header-info">
 | 
			
		||||
            <div class="breadcrumb">
 | 
			
		||||
              ${previousItem
 | 
			
		||||
                          </mwc-fab>
 | 
			
		||||
                        `
 | 
			
		||||
                      : ""}
 | 
			
		||||
                  </div>
 | 
			
		||||
                `
 | 
			
		||||
              : html``}
 | 
			
		||||
            <div class="header-info">
 | 
			
		||||
              <div class="breadcrumb">
 | 
			
		||||
                ${previousItem
 | 
			
		||||
                  ? html`
 | 
			
		||||
                      <div class="previous-title" @click=${this.navigateBack}>
 | 
			
		||||
                        <ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
 | 
			
		||||
                        ${previousItem.title}
 | 
			
		||||
                      </div>
 | 
			
		||||
                    `
 | 
			
		||||
                  : ""}
 | 
			
		||||
                <h1 class="title">${currentItem.title}</h1>
 | 
			
		||||
                ${subtitle
 | 
			
		||||
                  ? html`
 | 
			
		||||
                      <h2 class="subtitle">
 | 
			
		||||
                        ${subtitle}
 | 
			
		||||
                      </h2>
 | 
			
		||||
                    `
 | 
			
		||||
                  : ""}
 | 
			
		||||
              </div>
 | 
			
		||||
              ${currentItem.can_play &&
 | 
			
		||||
              (!currentItem.thumbnail || !this._narrow)
 | 
			
		||||
                ? html`
 | 
			
		||||
                    <div class="previous-title" @click=${this.navigateBack}>
 | 
			
		||||
                      <ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
 | 
			
		||||
                      ${previousItem.title}
 | 
			
		||||
                    </div>
 | 
			
		||||
                  `
 | 
			
		||||
                : ""}
 | 
			
		||||
              <h1 class="title">${currentItem.title}</h1>
 | 
			
		||||
              ${mediaType
 | 
			
		||||
                ? html`
 | 
			
		||||
                    <h2 class="subtitle">
 | 
			
		||||
                      ${mediaType}
 | 
			
		||||
                    </h2>
 | 
			
		||||
                    <mwc-button
 | 
			
		||||
                      raised
 | 
			
		||||
                      .item=${currentItem}
 | 
			
		||||
                      @click=${this._actionClicked}
 | 
			
		||||
                    >
 | 
			
		||||
                      <ha-svg-icon
 | 
			
		||||
                        .label=${this.hass.localize(
 | 
			
		||||
                          `ui.components.media-browser.${this.action}-media`
 | 
			
		||||
                        )}
 | 
			
		||||
                        .path=${this.action === "play" ? mdiPlay : mdiPlus}
 | 
			
		||||
                      ></ha-svg-icon>
 | 
			
		||||
                      ${this.hass.localize(
 | 
			
		||||
                        `ui.components.media-browser.${this.action}`
 | 
			
		||||
                      )}
 | 
			
		||||
                    </mwc-button>
 | 
			
		||||
                  `
 | 
			
		||||
                : ""}
 | 
			
		||||
            </div>
 | 
			
		||||
            ${currentItem.can_play && (!currentItem.thumbnail || !this._narrow)
 | 
			
		||||
              ? html`
 | 
			
		||||
                  <mwc-button
 | 
			
		||||
                    raised
 | 
			
		||||
                    .item=${currentItem}
 | 
			
		||||
                    @click=${this._actionClicked}
 | 
			
		||||
                  >
 | 
			
		||||
                    <ha-svg-icon
 | 
			
		||||
                      slot="icon"
 | 
			
		||||
                      .label=${this.hass.localize(
 | 
			
		||||
                        `ui.components.media-browser.${this.action}-media`
 | 
			
		||||
                      )}
 | 
			
		||||
                      .path=${this.action === "play" ? mdiPlay : mdiPlus}
 | 
			
		||||
                    ></ha-svg-icon>
 | 
			
		||||
                    ${this.hass.localize(
 | 
			
		||||
                      `ui.components.media-browser.${this.action}`
 | 
			
		||||
                    )}
 | 
			
		||||
                  </mwc-button>
 | 
			
		||||
                `
 | 
			
		||||
              : ""}
 | 
			
		||||
          </div>
 | 
			
		||||
          ${this.dialog
 | 
			
		||||
            ? html`
 | 
			
		||||
                <mwc-icon-button
 | 
			
		||||
                  aria-label=${this.hass.localize("ui.dialogs.generic.close")}
 | 
			
		||||
                  @click=${this._closeDialogAction}
 | 
			
		||||
                  class="header_button"
 | 
			
		||||
                  dir=${computeRTLDirection(this.hass)}
 | 
			
		||||
                >
 | 
			
		||||
                  <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
 | 
			
		||||
                </mwc-icon-button>
 | 
			
		||||
              `
 | 
			
		||||
            : ""}
 | 
			
		||||
        </div>
 | 
			
		||||
        ${this.dialog
 | 
			
		||||
          ? html`
 | 
			
		||||
              <mwc-icon-button
 | 
			
		||||
                aria-label=${this.hass.localize("ui.dialogs.generic.close")}
 | 
			
		||||
                @click=${this._closeDialogAction}
 | 
			
		||||
                class="header_button"
 | 
			
		||||
                dir=${computeRTLDirection(this.hass)}
 | 
			
		||||
              >
 | 
			
		||||
                <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
 | 
			
		||||
              </mwc-icon-button>
 | 
			
		||||
            `
 | 
			
		||||
          : ""}
 | 
			
		||||
      </div>
 | 
			
		||||
      ${currentItem.children?.length
 | 
			
		||||
        ? hasExpandableChildren
 | 
			
		||||
      ${this._error
 | 
			
		||||
        ? html`
 | 
			
		||||
            <div class="container error">
 | 
			
		||||
              ${this._renderError(this._error)}
 | 
			
		||||
            </div>
 | 
			
		||||
          `
 | 
			
		||||
        : currentItem.children?.length
 | 
			
		||||
        ? childrenMediaClass.layout === "grid"
 | 
			
		||||
          ? html`
 | 
			
		||||
              <div class="children">
 | 
			
		||||
              <div
 | 
			
		||||
                class="children ${classMap({
 | 
			
		||||
                  portrait: childrenMediaClass.thumbnail_ratio === "portrait",
 | 
			
		||||
                })}"
 | 
			
		||||
              >
 | 
			
		||||
                ${currentItem.children.map(
 | 
			
		||||
                  (child) => html`
 | 
			
		||||
                    <div
 | 
			
		||||
                      class="child"
 | 
			
		||||
                      .item=${child}
 | 
			
		||||
                      @click=${this._navigateForward}
 | 
			
		||||
                      @click=${this._childClicked}
 | 
			
		||||
                    >
 | 
			
		||||
                      <div class="ha-card-parent">
 | 
			
		||||
                        <ha-card
 | 
			
		||||
                          outlined
 | 
			
		||||
                          style="background-image: url(${child.thumbnail})"
 | 
			
		||||
                          style=${styleMap({
 | 
			
		||||
                            backgroundImage: child.thumbnail
 | 
			
		||||
                              ? `url(${child.thumbnail})`
 | 
			
		||||
                              : "none",
 | 
			
		||||
                          })}
 | 
			
		||||
                        >
 | 
			
		||||
                          ${child.can_expand && !child.thumbnail
 | 
			
		||||
                          ${!child.thumbnail
 | 
			
		||||
                            ? html`
 | 
			
		||||
                                <ha-svg-icon
 | 
			
		||||
                                  class="folder"
 | 
			
		||||
                                  .path=${mdiFolder}
 | 
			
		||||
                                  .path=${MediaClassBrowserSettings[
 | 
			
		||||
                                    child.media_class === "directory"
 | 
			
		||||
                                      ? child.children_media_class ||
 | 
			
		||||
                                        child.media_class
 | 
			
		||||
                                      : child.media_class
 | 
			
		||||
                                  ].icon}
 | 
			
		||||
                                ></ha-svg-icon>
 | 
			
		||||
                              `
 | 
			
		||||
                            : ""}
 | 
			
		||||
@@ -237,7 +280,9 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
                        ${child.can_play
 | 
			
		||||
                          ? html`
 | 
			
		||||
                              <mwc-icon-button
 | 
			
		||||
                                class="play"
 | 
			
		||||
                                class="play ${classMap({
 | 
			
		||||
                                  can_expand: child.can_expand,
 | 
			
		||||
                                })}"
 | 
			
		||||
                                .item=${child}
 | 
			
		||||
                                .label=${this.hass.localize(
 | 
			
		||||
                                  `ui.components.media-browser.${this.action}-media`
 | 
			
		||||
@@ -269,15 +314,16 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
                ${currentItem.children.map(
 | 
			
		||||
                  (child) => html`
 | 
			
		||||
                    <mwc-list-item
 | 
			
		||||
                      @click=${this._actionClicked}
 | 
			
		||||
                      @click=${this._childClicked}
 | 
			
		||||
                      .item=${child}
 | 
			
		||||
                      graphic="avatar"
 | 
			
		||||
                      hasMeta
 | 
			
		||||
                      dir=${computeRTLDirection(this.hass)}
 | 
			
		||||
                    >
 | 
			
		||||
                      <div
 | 
			
		||||
                        class="graphic"
 | 
			
		||||
                        style=${ifDefined(
 | 
			
		||||
                          showImages && child.thumbnail
 | 
			
		||||
                          mediaClass.show_list_images && child.thumbnail
 | 
			
		||||
                            ? `background-image: url(${child.thumbnail})`
 | 
			
		||||
                            : undefined
 | 
			
		||||
                        )}
 | 
			
		||||
@@ -285,7 +331,8 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
                      >
 | 
			
		||||
                        <mwc-icon-button
 | 
			
		||||
                          class="play ${classMap({
 | 
			
		||||
                            show: !showImages || !child.thumbnail,
 | 
			
		||||
                            show:
 | 
			
		||||
                              !mediaClass.show_list_images || !child.thumbnail,
 | 
			
		||||
                          })}"
 | 
			
		||||
                          .item=${child}
 | 
			
		||||
                          .label=${this.hass.localize(
 | 
			
		||||
@@ -298,14 +345,18 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
                          ></ha-svg-icon>
 | 
			
		||||
                        </mwc-icon-button>
 | 
			
		||||
                      </div>
 | 
			
		||||
                      <span>${child.title}</span>
 | 
			
		||||
                      <span class="title">${child.title}</span>
 | 
			
		||||
                    </mwc-list-item>
 | 
			
		||||
                    <li divider role="separator"></li>
 | 
			
		||||
                  `
 | 
			
		||||
                )}
 | 
			
		||||
              </mwc-list>
 | 
			
		||||
            `
 | 
			
		||||
        : this.hass.localize("ui.components.media-browser.no_items")}
 | 
			
		||||
        : html`
 | 
			
		||||
            <div class="container">
 | 
			
		||||
              ${this.hass.localize("ui.components.media-browser.no_items")}
 | 
			
		||||
            </div>
 | 
			
		||||
          `}
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -331,11 +382,22 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this._fetchData(this.mediaContentId, this.mediaContentType).then(
 | 
			
		||||
      (itemData) => {
 | 
			
		||||
    if (changedProps.has("entityId")) {
 | 
			
		||||
      this._error = undefined;
 | 
			
		||||
      this._mediaPlayerItems = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this._fetchData(this.mediaContentId, this.mediaContentType)
 | 
			
		||||
      .then((itemData) => {
 | 
			
		||||
        if (!itemData) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this._mediaPlayerItems = [itemData];
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
      })
 | 
			
		||||
      .catch((err) => {
 | 
			
		||||
        this._error = err;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _actionClicked(ev: MouseEvent): void {
 | 
			
		||||
@@ -349,21 +411,41 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
    fireEvent(this, "media-picked", { item });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _navigateForward(ev: MouseEvent): Promise<void> {
 | 
			
		||||
  private async _childClicked(ev: MouseEvent): Promise<void> {
 | 
			
		||||
    const target = ev.currentTarget as any;
 | 
			
		||||
    const item: MediaPlayerItem = target.item;
 | 
			
		||||
 | 
			
		||||
    if (!item) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!item.can_expand) {
 | 
			
		||||
      this._runAction(item);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this._navigate(item);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _navigate(item: MediaPlayerItem) {
 | 
			
		||||
    const itemData = await this._fetchData(
 | 
			
		||||
      item.media_content_id,
 | 
			
		||||
      item.media_content_type
 | 
			
		||||
    );
 | 
			
		||||
    this._error = undefined;
 | 
			
		||||
 | 
			
		||||
    let itemData: MediaPlayerItem;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      itemData = await this._fetchData(
 | 
			
		||||
        item.media_content_id,
 | 
			
		||||
        item.media_content_type
 | 
			
		||||
      );
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      showAlertDialog(this, {
 | 
			
		||||
        title: this.hass.localize(
 | 
			
		||||
          "ui.components.media-browser.media_browsing_error"
 | 
			
		||||
        ),
 | 
			
		||||
        text: this._renderError(err),
 | 
			
		||||
      });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.scrollTo(0, 0);
 | 
			
		||||
    this._mediaPlayerItems = [...this._mediaPlayerItems, itemData];
 | 
			
		||||
@@ -409,14 +491,38 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
    this._resizeObserver.observe(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _hasExpandableChildren = memoizeOne((children) =>
 | 
			
		||||
    children.find((item: MediaPlayerItem) => item.can_expand)
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  private _closeDialogAction(): void {
 | 
			
		||||
    fireEvent(this, "close-dialog");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _renderError(err: { message: string; code: string }) {
 | 
			
		||||
    if (err.message === "Media directory does not exist.") {
 | 
			
		||||
      return html`
 | 
			
		||||
        <h2>No local media found.</h2>
 | 
			
		||||
        <p>
 | 
			
		||||
          It looks like you have not yet created a media directory.
 | 
			
		||||
          <br />Create a directory with the name <b>"media"</b> in the
 | 
			
		||||
          configuration directory of Home Assistant
 | 
			
		||||
          (${this.hass.config.config_dir}). <br />Place your video, audio and
 | 
			
		||||
          image files in this directory to be able to browse and play them in
 | 
			
		||||
          the browser or on supported media players.
 | 
			
		||||
        </p>
 | 
			
		||||
 | 
			
		||||
        <p>
 | 
			
		||||
          Check the
 | 
			
		||||
          <a
 | 
			
		||||
            href="https://www.home-assistant.io/integrations/media_source/#local-media"
 | 
			
		||||
            target="_blank"
 | 
			
		||||
            rel="noreferrer"
 | 
			
		||||
            >documentation</a
 | 
			
		||||
          >
 | 
			
		||||
          for more info
 | 
			
		||||
        </p>
 | 
			
		||||
      `;
 | 
			
		||||
    }
 | 
			
		||||
    return html`<span class="error">err.message</span>`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get styles(): CSSResultArray {
 | 
			
		||||
    return [
 | 
			
		||||
      haStyle,
 | 
			
		||||
@@ -429,26 +535,26 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
          flex-direction: column;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .container {
 | 
			
		||||
          padding: 16px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .header {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          display: block;
 | 
			
		||||
          justify-content: space-between;
 | 
			
		||||
          border-bottom: 1px solid var(--divider-color);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .header_button {
 | 
			
		||||
          position: relative;
 | 
			
		||||
          top: 14px;
 | 
			
		||||
          right: -8px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .header {
 | 
			
		||||
          background-color: var(--card-background-color);
 | 
			
		||||
          position: sticky;
 | 
			
		||||
          position: -webkit-sticky;
 | 
			
		||||
          top: 0;
 | 
			
		||||
          z-index: 5;
 | 
			
		||||
          padding: 20px 24px 10px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .header-wrapper {
 | 
			
		||||
          display: flex;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .header-content {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          flex-wrap: wrap;
 | 
			
		||||
@@ -476,6 +582,7 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
 | 
			
		||||
        .header-info mwc-button {
 | 
			
		||||
          display: block;
 | 
			
		||||
          --mdc-theme-primary: var(--primary-color);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .breadcrumb {
 | 
			
		||||
@@ -519,6 +626,7 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
 | 
			
		||||
        mwc-list {
 | 
			
		||||
          --mdc-list-vertical-padding: 0;
 | 
			
		||||
          --mdc-list-item-graphic-margin: 0;
 | 
			
		||||
          --mdc-theme-text-icon-on-background: var(--secondary-text-color);
 | 
			
		||||
          margin-top: 10px;
 | 
			
		||||
        }
 | 
			
		||||
@@ -535,14 +643,18 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
          display: grid;
 | 
			
		||||
          grid-template-columns: repeat(
 | 
			
		||||
            auto-fit,
 | 
			
		||||
            minmax(var(--media-browse-item-size, 175px), 0.33fr)
 | 
			
		||||
            minmax(var(--media-browse-item-size, 175px), 0.1fr)
 | 
			
		||||
          );
 | 
			
		||||
          grid-gap: 16px;
 | 
			
		||||
          margin: 8px 0px;
 | 
			
		||||
          padding: 0px 24px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host(:not([narrow])) .children {
 | 
			
		||||
          padding: 0px 24px;
 | 
			
		||||
        :host([dialog]) .children {
 | 
			
		||||
          grid-template-columns: repeat(
 | 
			
		||||
            auto-fit,
 | 
			
		||||
            minmax(var(--media-browse-item-size, 175px), 0.33fr)
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .child {
 | 
			
		||||
@@ -556,7 +668,7 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
          width: 100%;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ha-card {
 | 
			
		||||
        .children ha-card {
 | 
			
		||||
          width: 100%;
 | 
			
		||||
          padding-bottom: 100%;
 | 
			
		||||
          position: relative;
 | 
			
		||||
@@ -564,6 +676,11 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
          background-size: cover;
 | 
			
		||||
          background-repeat: no-repeat;
 | 
			
		||||
          background-position: center;
 | 
			
		||||
          transition: padding-bottom 0.1s ease-out;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .portrait.children ha-card {
 | 
			
		||||
          padding-bottom: 150%;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .child .folder,
 | 
			
		||||
@@ -579,24 +696,43 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .child .play {
 | 
			
		||||
          transition: color 0.5s;
 | 
			
		||||
          border-radius: 50%;
 | 
			
		||||
          bottom: calc(50% - 35px);
 | 
			
		||||
          right: calc(50% - 35px);
 | 
			
		||||
          opacity: 0;
 | 
			
		||||
          transition: opacity 0.1s ease-out;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .child .play:not(.can_expand) {
 | 
			
		||||
          --mdc-icon-button-size: 70px;
 | 
			
		||||
          --mdc-icon-size: 48px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .ha-card-parent:hover .play:not(.can_expand) {
 | 
			
		||||
          opacity: 1;
 | 
			
		||||
          color: var(--primary-color);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .child .play.can_expand {
 | 
			
		||||
          opacity: 1;
 | 
			
		||||
          background-color: rgba(var(--rgb-card-background-color), 0.5);
 | 
			
		||||
          bottom: 4px;
 | 
			
		||||
          right: 4px;
 | 
			
		||||
          transition: all 0.5s;
 | 
			
		||||
          background-color: rgba(var(--rgb-card-background-color), 0.5);
 | 
			
		||||
          border-radius: 50%;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .child .play:hover {
 | 
			
		||||
          color: var(--primary-color);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ha-card:hover {
 | 
			
		||||
        .ha-card-parent:hover ha-card {
 | 
			
		||||
          opacity: 0.5;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .child .title {
 | 
			
		||||
          font-size: 16px;
 | 
			
		||||
          padding-top: 8px;
 | 
			
		||||
          padding-left: 2px;
 | 
			
		||||
          overflow: hidden;
 | 
			
		||||
          display: -webkit-box;
 | 
			
		||||
          -webkit-box-orient: vertical;
 | 
			
		||||
@@ -606,6 +742,7 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
        .child .type {
 | 
			
		||||
          font-size: 12px;
 | 
			
		||||
          color: var(--secondary-text-color);
 | 
			
		||||
          padding-left: 2px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mwc-list-item .graphic {
 | 
			
		||||
@@ -630,6 +767,14 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
          background-color: transparent;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mwc-list-item .title {
 | 
			
		||||
          margin-left: 16px;
 | 
			
		||||
        }
 | 
			
		||||
        mwc-list-item[dir="rtl"] .title {
 | 
			
		||||
          margin-right: 16px;
 | 
			
		||||
          margin-left: 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* ============= Narrow ============= */
 | 
			
		||||
 | 
			
		||||
        :host([narrow]) {
 | 
			
		||||
@@ -644,6 +789,10 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
          padding: 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host([narrow]) .header.no-dialog {
 | 
			
		||||
          display: block;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host([narrow]) .header_button {
 | 
			
		||||
          position: absolute;
 | 
			
		||||
          top: 14px;
 | 
			
		||||
@@ -683,8 +832,7 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
          padding: 20px 24px 10px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host([narrow]) .media-source,
 | 
			
		||||
        :host([narrow]) .children {
 | 
			
		||||
        :host([narrow]) .media-source {
 | 
			
		||||
          padding: 0 24px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -703,8 +851,8 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
			
		||||
          -webkit-line-clamp: 1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host(:not([narrow])[scroll]) .header-info {
 | 
			
		||||
          height: 75px;
 | 
			
		||||
        :host(:not([narrow])[scroll]) .header:not(.no-img) mwc-icon-button {
 | 
			
		||||
          align-self: center;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host([scroll]) .header-info mwc-button,
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,9 @@ class PersonBadge extends LitElement {
 | 
			
		||||
 | 
			
		||||
  static get styles(): CSSResult {
 | 
			
		||||
    return css`
 | 
			
		||||
      :host {
 | 
			
		||||
        display: contents;
 | 
			
		||||
      }
 | 
			
		||||
      .picture {
 | 
			
		||||
        width: 40px;
 | 
			
		||||
        height: 40px;
 | 
			
		||||
 
 | 
			
		||||
@@ -104,6 +104,9 @@ class UserBadge extends LitElement {
 | 
			
		||||
 | 
			
		||||
  static get styles(): CSSResult {
 | 
			
		||||
    return css`
 | 
			
		||||
      :host {
 | 
			
		||||
        display: contents;
 | 
			
		||||
      }
 | 
			
		||||
      .picture {
 | 
			
		||||
        width: 40px;
 | 
			
		||||
        height: 40px;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import {
 | 
			
		||||
  HassEntityBase,
 | 
			
		||||
} from "home-assistant-js-websocket";
 | 
			
		||||
import { navigate } from "../common/navigate";
 | 
			
		||||
import { HomeAssistant, Context } from "../types";
 | 
			
		||||
import { Context, HomeAssistant } from "../types";
 | 
			
		||||
import { DeviceCondition, DeviceTrigger } from "./device_automation";
 | 
			
		||||
import { Action } from "./script";
 | 
			
		||||
 | 
			
		||||
@@ -15,6 +15,7 @@ export interface AutomationEntity extends HassEntityBase {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AutomationConfig {
 | 
			
		||||
  id?: string;
 | 
			
		||||
  alias: string;
 | 
			
		||||
  description: string;
 | 
			
		||||
  trigger: Trigger[];
 | 
			
		||||
@@ -32,7 +33,8 @@ export interface ForDict {
 | 
			
		||||
 | 
			
		||||
export interface StateTrigger {
 | 
			
		||||
  platform: "state";
 | 
			
		||||
  entity_id?: string;
 | 
			
		||||
  entity_id: string;
 | 
			
		||||
  attribute?: string;
 | 
			
		||||
  from?: string | number;
 | 
			
		||||
  to?: string | number;
 | 
			
		||||
  for?: string | number | ForDict;
 | 
			
		||||
@@ -59,6 +61,7 @@ export interface HassTrigger {
 | 
			
		||||
export interface NumericStateTrigger {
 | 
			
		||||
  platform: "numeric_state";
 | 
			
		||||
  entity_id: string;
 | 
			
		||||
  attribute?: string;
 | 
			
		||||
  above?: number;
 | 
			
		||||
  below?: number;
 | 
			
		||||
  value_template?: string;
 | 
			
		||||
@@ -136,12 +139,14 @@ export interface LogicalCondition {
 | 
			
		||||
export interface StateCondition {
 | 
			
		||||
  condition: "state";
 | 
			
		||||
  entity_id: string;
 | 
			
		||||
  attribute?: string;
 | 
			
		||||
  state: string | number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface NumericStateCondition {
 | 
			
		||||
  condition: "numeric_state";
 | 
			
		||||
  entity_id: string;
 | 
			
		||||
  attribute?: string;
 | 
			
		||||
  above?: number;
 | 
			
		||||
  below?: number;
 | 
			
		||||
  value_template?: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,8 @@ export const DISCOVERY_SOURCES = [
 | 
			
		||||
  "discovery",
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const ATTENTION_SOURCES = ["reauth"];
 | 
			
		||||
 | 
			
		||||
export const createConfigFlow = (hass: HomeAssistant, handler: string) =>
 | 
			
		||||
  hass.callApi<DataEntryFlowStep>("POST", "config/config_entries/flow", {
 | 
			
		||||
    handler,
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,9 @@ export const hassioApiResultExtractor = <T>(response: HassioResponse<T>) =>
 | 
			
		||||
export const extractApiErrorMessage = (error: any): string => {
 | 
			
		||||
  return typeof error === "object"
 | 
			
		||||
    ? typeof error.body === "object"
 | 
			
		||||
      ? error.body.message || "Unkown error, see logs"
 | 
			
		||||
      : error.body || "Unkown error, see logs"
 | 
			
		||||
      ? error.body.message || "Unknown error, see logs"
 | 
			
		||||
      : error.body || "Unknown error, see logs"
 | 
			
		||||
    : error;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ignoredStatusCodes = new Set([502, 503, 504]);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,23 @@
 | 
			
		||||
import type { HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
import type { HomeAssistant } from "../types";
 | 
			
		||||
import {
 | 
			
		||||
  mdiFolder,
 | 
			
		||||
  mdiPlaylistMusic,
 | 
			
		||||
  mdiFileMusic,
 | 
			
		||||
  mdiAlbum,
 | 
			
		||||
  mdiMusic,
 | 
			
		||||
  mdiTelevisionClassic,
 | 
			
		||||
  mdiMovie,
 | 
			
		||||
  mdiVideo,
 | 
			
		||||
  mdiImage,
 | 
			
		||||
  mdiWeb,
 | 
			
		||||
  mdiGamepadVariant,
 | 
			
		||||
  mdiAccountMusic,
 | 
			
		||||
  mdiPodcast,
 | 
			
		||||
  mdiApplication,
 | 
			
		||||
  mdiAccountMusicOutline,
 | 
			
		||||
  mdiDramaMasks,
 | 
			
		||||
} from "@mdi/js";
 | 
			
		||||
 | 
			
		||||
export const SUPPORT_PAUSE = 1;
 | 
			
		||||
export const SUPPORT_SEEK = 2;
 | 
			
		||||
@@ -22,6 +40,66 @@ export type MediaPlayerBrowseAction = "pick" | "play";
 | 
			
		||||
 | 
			
		||||
export const BROWSER_SOURCE = "browser";
 | 
			
		||||
 | 
			
		||||
export type MediaClassBrowserSetting = {
 | 
			
		||||
  icon: string;
 | 
			
		||||
  thumbnail_ratio?: string;
 | 
			
		||||
  layout?: string;
 | 
			
		||||
  show_list_images?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const MediaClassBrowserSettings: {
 | 
			
		||||
  [type: string]: MediaClassBrowserSetting;
 | 
			
		||||
} = {
 | 
			
		||||
  album: { icon: mdiAlbum, layout: "grid" },
 | 
			
		||||
  app: { icon: mdiApplication, layout: "grid" },
 | 
			
		||||
  artist: { icon: mdiAccountMusic, layout: "grid", show_list_images: true },
 | 
			
		||||
  channel: {
 | 
			
		||||
    icon: mdiTelevisionClassic,
 | 
			
		||||
    thumbnail_ratio: "portrait",
 | 
			
		||||
    layout: "grid",
 | 
			
		||||
  },
 | 
			
		||||
  composer: {
 | 
			
		||||
    icon: mdiAccountMusicOutline,
 | 
			
		||||
    layout: "grid",
 | 
			
		||||
    show_list_images: true,
 | 
			
		||||
  },
 | 
			
		||||
  contributing_artist: {
 | 
			
		||||
    icon: mdiAccountMusic,
 | 
			
		||||
    layout: "grid",
 | 
			
		||||
    show_list_images: true,
 | 
			
		||||
  },
 | 
			
		||||
  directory: { icon: mdiFolder, layout: "grid", show_list_images: true },
 | 
			
		||||
  episode: {
 | 
			
		||||
    icon: mdiTelevisionClassic,
 | 
			
		||||
    layout: "grid",
 | 
			
		||||
    thumbnail_ratio: "portrait",
 | 
			
		||||
  },
 | 
			
		||||
  game: {
 | 
			
		||||
    icon: mdiGamepadVariant,
 | 
			
		||||
    layout: "grid",
 | 
			
		||||
    thumbnail_ratio: "portrait",
 | 
			
		||||
  },
 | 
			
		||||
  genre: { icon: mdiDramaMasks, layout: "grid", show_list_images: true },
 | 
			
		||||
  image: { icon: mdiImage, layout: "grid" },
 | 
			
		||||
  movie: { icon: mdiMovie, thumbnail_ratio: "portrait", layout: "grid" },
 | 
			
		||||
  music: { icon: mdiMusic },
 | 
			
		||||
  playlist: { icon: mdiPlaylistMusic, layout: "grid", show_list_images: true },
 | 
			
		||||
  podcast: { icon: mdiPodcast, layout: "grid" },
 | 
			
		||||
  season: {
 | 
			
		||||
    icon: mdiTelevisionClassic,
 | 
			
		||||
    layout: "grid",
 | 
			
		||||
    thumbnail_ratio: "portrait",
 | 
			
		||||
  },
 | 
			
		||||
  track: { icon: mdiFileMusic },
 | 
			
		||||
  tv_show: {
 | 
			
		||||
    icon: mdiTelevisionClassic,
 | 
			
		||||
    layout: "grid",
 | 
			
		||||
    thumbnail_ratio: "portrait",
 | 
			
		||||
  },
 | 
			
		||||
  url: { icon: mdiWeb },
 | 
			
		||||
  video: { icon: mdiVideo, layout: "grid" },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface MediaPickedEvent {
 | 
			
		||||
  item: MediaPlayerItem;
 | 
			
		||||
}
 | 
			
		||||
@@ -40,6 +118,8 @@ export interface MediaPlayerItem {
 | 
			
		||||
  title: string;
 | 
			
		||||
  media_content_type: string;
 | 
			
		||||
  media_content_id: string;
 | 
			
		||||
  media_class: string;
 | 
			
		||||
  children_media_class: string;
 | 
			
		||||
  can_play: boolean;
 | 
			
		||||
  can_expand: boolean;
 | 
			
		||||
  thumbnail?: string;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								src/data/refresh_token.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/data/refresh_token.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HASSDomEvents {
 | 
			
		||||
    "hass-refresh-tokens": undefined;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RefreshToken {
 | 
			
		||||
  client_icon?: string;
 | 
			
		||||
  client_id: string;
 | 
			
		||||
  client_name?: string;
 | 
			
		||||
  created_at: string;
 | 
			
		||||
  id: string;
 | 
			
		||||
  is_current: boolean;
 | 
			
		||||
  last_used_at?: string;
 | 
			
		||||
  last_used_ip?: string;
 | 
			
		||||
  type: "normal" | "long_lived_access_token";
 | 
			
		||||
}
 | 
			
		||||
@@ -5,7 +5,7 @@ import {
 | 
			
		||||
import { computeObjectId } from "../common/entity/compute_object_id";
 | 
			
		||||
import { navigate } from "../common/navigate";
 | 
			
		||||
import { HomeAssistant } from "../types";
 | 
			
		||||
import { Condition } from "./automation";
 | 
			
		||||
import { Condition, Trigger } from "./automation";
 | 
			
		||||
 | 
			
		||||
export const MODES = ["single", "restart", "queued", "parallel"];
 | 
			
		||||
export const MODES_MAX = ["queued", "parallel"];
 | 
			
		||||
@@ -56,6 +56,13 @@ export interface SceneAction {
 | 
			
		||||
export interface WaitAction {
 | 
			
		||||
  wait_template: string;
 | 
			
		||||
  timeout?: number;
 | 
			
		||||
  continue_on_timeout?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface WaitForTriggerAction {
 | 
			
		||||
  wait_for_trigger: Trigger[];
 | 
			
		||||
  timeout?: number;
 | 
			
		||||
  continue_on_timeout?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RepeatAction {
 | 
			
		||||
@@ -91,6 +98,7 @@ export type Action =
 | 
			
		||||
  | DelayAction
 | 
			
		||||
  | SceneAction
 | 
			
		||||
  | WaitAction
 | 
			
		||||
  | WaitForTriggerAction
 | 
			
		||||
  | RepeatAction
 | 
			
		||||
  | ChooseAction;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -200,7 +200,7 @@ export const weatherSVGStyles = css`
 | 
			
		||||
    fill: var(--weather-icon-sun-color, #fdd93c);
 | 
			
		||||
  }
 | 
			
		||||
  .moon {
 | 
			
		||||
    fill: var(--weather-icon-moon-color, #fdf9cc);
 | 
			
		||||
    fill: var(--weather-icon-moon-color, #fcf497);
 | 
			
		||||
  }
 | 
			
		||||
  .cloud-back {
 | 
			
		||||
    fill: var(--weather-icon-cloud-back-color, #d4d4d4);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,27 @@
 | 
			
		||||
import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
			
		||||
 | 
			
		||||
interface RenderTemplateResult {
 | 
			
		||||
export interface RenderTemplateResult {
 | 
			
		||||
  result: string;
 | 
			
		||||
  listeners: TemplateListeners;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface TemplateListeners {
 | 
			
		||||
  all: boolean;
 | 
			
		||||
  domains: string[];
 | 
			
		||||
  entities: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const subscribeRenderTemplate = (
 | 
			
		||||
  conn: Connection,
 | 
			
		||||
  onChange: (result: string) => void,
 | 
			
		||||
  onChange: (result: RenderTemplateResult) => void,
 | 
			
		||||
  params: {
 | 
			
		||||
    template: string;
 | 
			
		||||
    entity_ids?: string | string[];
 | 
			
		||||
    variables?: object;
 | 
			
		||||
  }
 | 
			
		||||
): Promise<UnsubscribeFunc> => {
 | 
			
		||||
  return conn.subscribeMessage(
 | 
			
		||||
    (msg: RenderTemplateResult) => onChange(msg.result),
 | 
			
		||||
    { type: "render_template", ...params }
 | 
			
		||||
  );
 | 
			
		||||
  return conn.subscribeMessage((msg: RenderTemplateResult) => onChange(msg), {
 | 
			
		||||
    type: "render_template",
 | 
			
		||||
    ...params,
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -97,8 +97,13 @@ export const showConfigFlowDialog = (
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    renderExternalStepHeader(hass, step) {
 | 
			
		||||
      return hass.localize(
 | 
			
		||||
        `component.${step.handler}.config.step.${step.step_id}.title`
 | 
			
		||||
      return (
 | 
			
		||||
        hass.localize(
 | 
			
		||||
          `component.${step.handler}.config.step.${step.step_id}.title`
 | 
			
		||||
        ) ||
 | 
			
		||||
        hass.localize(
 | 
			
		||||
          "ui.panel.config.integrations.config_flow.external_step.open_site"
 | 
			
		||||
        )
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,19 +5,19 @@ import {
 | 
			
		||||
  CSSResult,
 | 
			
		||||
  customElement,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { classMap } from "lit-html/directives/class-map";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
import "../../components/ha-dialog";
 | 
			
		||||
import "../../components/ha-switch";
 | 
			
		||||
import { PolymerChangedEvent } from "../../polymer-types";
 | 
			
		||||
import { haStyleDialog } from "../../resources/styles";
 | 
			
		||||
import { HomeAssistant } from "../../types";
 | 
			
		||||
import { DialogParams } from "./show-dialog-box";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
 | 
			
		||||
@customElement("dialog-box")
 | 
			
		||||
class DialogBox extends LitElement {
 | 
			
		||||
@@ -57,7 +57,8 @@ class DialogBox extends LitElement {
 | 
			
		||||
        open
 | 
			
		||||
        ?scrimClickAction=${this._params.prompt}
 | 
			
		||||
        ?escapeKeyAction=${this._params.prompt}
 | 
			
		||||
        @closed=${this._dismiss}
 | 
			
		||||
        @closed=${this._dialogClosed}
 | 
			
		||||
        defaultAction="ignore"
 | 
			
		||||
        .heading=${this._params.title
 | 
			
		||||
          ? this._params.title
 | 
			
		||||
          : this._params.confirmation &&
 | 
			
		||||
@@ -78,10 +79,10 @@ class DialogBox extends LitElement {
 | 
			
		||||
          ${this._params.prompt
 | 
			
		||||
            ? html`
 | 
			
		||||
                <paper-input
 | 
			
		||||
                  autofocus
 | 
			
		||||
                  dialogInitialFocus
 | 
			
		||||
                  .value=${this._value}
 | 
			
		||||
                  @value-changed=${this._valueChanged}
 | 
			
		||||
                  @keyup=${this._handleKeyUp}
 | 
			
		||||
                  @value-changed=${this._valueChanged}
 | 
			
		||||
                  .label=${this._params.inputLabel
 | 
			
		||||
                    ? this._params.inputLabel
 | 
			
		||||
                    : ""}
 | 
			
		||||
@@ -100,7 +101,11 @@ class DialogBox extends LitElement {
 | 
			
		||||
              : this.hass.localize("ui.dialogs.generic.cancel")}
 | 
			
		||||
          </mwc-button>
 | 
			
		||||
        `}
 | 
			
		||||
        <mwc-button @click=${this._confirm} slot="primaryAction">
 | 
			
		||||
        <mwc-button
 | 
			
		||||
          @click=${this._confirm}
 | 
			
		||||
          ?dialogInitialFocus=${!this._params.prompt}
 | 
			
		||||
          slot="primaryAction"
 | 
			
		||||
        >
 | 
			
		||||
          ${this._params.confirmText
 | 
			
		||||
            ? this._params.confirmText
 | 
			
		||||
            : this.hass.localize("ui.dialogs.generic.ok")}
 | 
			
		||||
@@ -114,8 +119,8 @@ class DialogBox extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _dismiss(): void {
 | 
			
		||||
    if (this._params!.cancel) {
 | 
			
		||||
      this._params!.cancel();
 | 
			
		||||
    if (this._params?.cancel) {
 | 
			
		||||
      this._params.cancel();
 | 
			
		||||
    }
 | 
			
		||||
    this._close();
 | 
			
		||||
  }
 | 
			
		||||
@@ -133,7 +138,17 @@ class DialogBox extends LitElement {
 | 
			
		||||
    this._close();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _dialogClosed(ev) {
 | 
			
		||||
    if (ev.detail.action === "ignore") {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.closeDialog();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _close(): void {
 | 
			
		||||
    if (!this._params) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this._params = undefined;
 | 
			
		||||
    fireEvent(this, "dialog-closed", { dialog: this.localName });
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -12,12 +12,13 @@ import {
 | 
			
		||||
import "../../../components/ha-relative-time";
 | 
			
		||||
import { triggerAutomation } from "../../../data/automation";
 | 
			
		||||
import { HomeAssistant } from "../../../types";
 | 
			
		||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
 | 
			
		||||
 | 
			
		||||
@customElement("more-info-automation")
 | 
			
		||||
class MoreInfoAutomation extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property() public stateObj?: HassEntity;
 | 
			
		||||
  @property({ attribute: false }) public stateObj?: HassEntity;
 | 
			
		||||
 | 
			
		||||
  protected render(): TemplateResult {
 | 
			
		||||
    if (!this.hass || !this.stateObj) {
 | 
			
		||||
@@ -34,7 +35,10 @@ class MoreInfoAutomation extends LitElement {
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="actions">
 | 
			
		||||
        <mwc-button @click=${this.handleAction}>
 | 
			
		||||
        <mwc-button
 | 
			
		||||
          @click=${this.handleAction}
 | 
			
		||||
          .disabled=${UNAVAILABLE_STATES.includes(this.stateObj!.state)}
 | 
			
		||||
        >
 | 
			
		||||
          ${this.hass.localize("ui.card.automation.trigger")}
 | 
			
		||||
        </mwc-button>
 | 
			
		||||
      </div>
 | 
			
		||||
@@ -52,7 +56,7 @@ class MoreInfoAutomation extends LitElement {
 | 
			
		||||
        justify-content: space-between;
 | 
			
		||||
      }
 | 
			
		||||
      .actions {
 | 
			
		||||
        margin: 36px 0 8px 0;
 | 
			
		||||
        margin: 8px 0;
 | 
			
		||||
        text-align: right;
 | 
			
		||||
      }
 | 
			
		||||
    `;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,9 @@ import {
 | 
			
		||||
  css,
 | 
			
		||||
  CSSResult,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  PropertyValues,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
@@ -47,8 +47,8 @@ class MoreInfoCamera extends LitElement {
 | 
			
		||||
    return html`
 | 
			
		||||
      <ha-camera-stream
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        .stateObj="${this.stateObj}"
 | 
			
		||||
        showcontrols
 | 
			
		||||
        .stateObj=${this.stateObj}
 | 
			
		||||
        controls
 | 
			
		||||
      ></ha-camera-stream>
 | 
			
		||||
      ${this._cameraPrefs
 | 
			
		||||
        ? html`
 | 
			
		||||
 
 | 
			
		||||
@@ -61,20 +61,20 @@ class MoreInfoLight extends LitElement {
 | 
			
		||||
          "is-on": this.stateObj.state === "on",
 | 
			
		||||
        })}"
 | 
			
		||||
      >
 | 
			
		||||
        ${supportsFeature(this.stateObj!, SUPPORT_BRIGHTNESS)
 | 
			
		||||
          ? html`
 | 
			
		||||
              <ha-labeled-slider
 | 
			
		||||
                caption=${this.hass.localize("ui.card.light.brightness")}
 | 
			
		||||
                icon="hass:brightness-5"
 | 
			
		||||
                min="1"
 | 
			
		||||
                max="255"
 | 
			
		||||
                value=${this._brightnessSliderValue}
 | 
			
		||||
                @change=${this._brightnessSliderChanged}
 | 
			
		||||
              ></ha-labeled-slider>
 | 
			
		||||
            `
 | 
			
		||||
          : ""}
 | 
			
		||||
        ${this.stateObj.state === "on"
 | 
			
		||||
          ? html`
 | 
			
		||||
              ${supportsFeature(this.stateObj!, SUPPORT_BRIGHTNESS)
 | 
			
		||||
                ? html`
 | 
			
		||||
                    <ha-labeled-slider
 | 
			
		||||
                      caption=${this.hass.localize("ui.card.light.brightness")}
 | 
			
		||||
                      icon="hass:brightness-5"
 | 
			
		||||
                      min="1"
 | 
			
		||||
                      max="255"
 | 
			
		||||
                      value=${this._brightnessSliderValue}
 | 
			
		||||
                      @change=${this._brightnessSliderChanged}
 | 
			
		||||
                    ></ha-labeled-slider>
 | 
			
		||||
                  `
 | 
			
		||||
                : ""}
 | 
			
		||||
              ${supportsFeature(this.stateObj, SUPPORT_COLOR_TEMP)
 | 
			
		||||
                ? html`
 | 
			
		||||
                    <ha-labeled-slider
 | 
			
		||||
@@ -134,7 +134,7 @@ class MoreInfoLight extends LitElement {
 | 
			
		||||
                        attr-for-selected="item-name"
 | 
			
		||||
                        >${this.stateObj.attributes.effect_list.map(
 | 
			
		||||
                          (effect: string) => html`
 | 
			
		||||
                            <paper-item itemName=${effect}
 | 
			
		||||
                            <paper-item .itemName=${effect}
 | 
			
		||||
                              >${effect}</paper-item
 | 
			
		||||
                            >
 | 
			
		||||
                          `
 | 
			
		||||
@@ -170,7 +170,7 @@ class MoreInfoLight extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _effectChanged(ev: CustomEvent) {
 | 
			
		||||
    const newVal = ev.detail.value;
 | 
			
		||||
    const newVal = ev.detail.item.itemName;
 | 
			
		||||
 | 
			
		||||
    if (!newVal || this.stateObj!.attributes.effect === newVal) {
 | 
			
		||||
      return;
 | 
			
		||||
 
 | 
			
		||||
@@ -130,7 +130,7 @@ class MoreInfoMediaPlayer extends LitElement {
 | 
			
		||||
            </div>
 | 
			
		||||
          `
 | 
			
		||||
        : ""}
 | 
			
		||||
      ${stateObj.state !== "off" &&
 | 
			
		||||
      ${![UNAVAILABLE, UNKNOWN, "off"].includes(stateObj.state) &&
 | 
			
		||||
      supportsFeature(stateObj, SUPPORT_SELECT_SOURCE) &&
 | 
			
		||||
      stateObj.attributes.source_list?.length
 | 
			
		||||
        ? html`
 | 
			
		||||
@@ -188,14 +188,17 @@ class MoreInfoMediaPlayer extends LitElement {
 | 
			
		||||
            <div class="tts">
 | 
			
		||||
              <paper-input
 | 
			
		||||
                id="ttsInput"
 | 
			
		||||
                .disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
 | 
			
		||||
                .label=${this.hass.localize(
 | 
			
		||||
                  "ui.card.media_player.text_to_speak"
 | 
			
		||||
                )}
 | 
			
		||||
                @keydown=${this._ttsCheckForEnter}
 | 
			
		||||
              ></paper-input>
 | 
			
		||||
              <ha-icon-button icon="hass:send" @click=${
 | 
			
		||||
                this._sendTTS
 | 
			
		||||
              }></ha-icon-button>
 | 
			
		||||
              <ha-icon-button 
 | 
			
		||||
                icon="hass:send"                 
 | 
			
		||||
                .disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
 | 
			
		||||
                @click=${this._sendTTS}
 | 
			
		||||
              ></ha-icon-button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          `
 | 
			
		||||
 
 | 
			
		||||
@@ -26,15 +26,12 @@ class MoreInfoTimer extends LitElement {
 | 
			
		||||
    return html`
 | 
			
		||||
      <ha-attributes
 | 
			
		||||
        .stateObj=${this.stateObj}
 | 
			
		||||
        .extraFilters=${"remaining"}
 | 
			
		||||
        extra-filters="remaining"
 | 
			
		||||
      ></ha-attributes>
 | 
			
		||||
      <div class="actions">
 | 
			
		||||
        ${this.stateObj.state === "idle" || this.stateObj.state === "paused"
 | 
			
		||||
          ? html`
 | 
			
		||||
              <mwc-button
 | 
			
		||||
                .action="${"start"}"
 | 
			
		||||
                @click="${this._handleActionClick}"
 | 
			
		||||
              >
 | 
			
		||||
              <mwc-button .action=${"start"} @click=${this._handleActionClick}>
 | 
			
		||||
                ${this.hass!.localize("ui.card.timer.actions.start")}
 | 
			
		||||
              </mwc-button>
 | 
			
		||||
            `
 | 
			
		||||
@@ -42,7 +39,7 @@ class MoreInfoTimer extends LitElement {
 | 
			
		||||
        ${this.stateObj.state === "active"
 | 
			
		||||
          ? html`
 | 
			
		||||
              <mwc-button
 | 
			
		||||
                .action="${"pause"}"
 | 
			
		||||
                .action=${"pause"}
 | 
			
		||||
                @click="${this._handleActionClick}"
 | 
			
		||||
              >
 | 
			
		||||
                ${this.hass!.localize("ui.card.timer.actions.pause")}
 | 
			
		||||
@@ -52,13 +49,13 @@ class MoreInfoTimer extends LitElement {
 | 
			
		||||
        ${this.stateObj.state === "active" || this.stateObj.state === "paused"
 | 
			
		||||
          ? html`
 | 
			
		||||
              <mwc-button
 | 
			
		||||
                .action="${"cancel"}"
 | 
			
		||||
                .action=${"cancel"}
 | 
			
		||||
                @click="${this._handleActionClick}"
 | 
			
		||||
              >
 | 
			
		||||
                ${this.hass!.localize("ui.card.timer.actions.cancel")}
 | 
			
		||||
              </mwc-button>
 | 
			
		||||
              <mwc-button
 | 
			
		||||
                .action="${"finish"}"
 | 
			
		||||
                .action=${"finish"}
 | 
			
		||||
                @click="${this._handleActionClick}"
 | 
			
		||||
              >
 | 
			
		||||
                ${this.hass!.localize("ui.card.timer.actions.finish")}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,10 +13,15 @@ import {
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { cache } from "lit-html/directives/cache";
 | 
			
		||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
 | 
			
		||||
import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const";
 | 
			
		||||
import {
 | 
			
		||||
  DOMAINS_MORE_INFO_NO_HISTORY,
 | 
			
		||||
  DOMAINS_WITH_MORE_INFO,
 | 
			
		||||
} from "../../common/const";
 | 
			
		||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
import { computeDomain } from "../../common/entity/compute_domain";
 | 
			
		||||
import { computeStateName } from "../../common/entity/compute_state_name";
 | 
			
		||||
import { stateMoreInfoType } from "../../common/entity/state_more_info_type";
 | 
			
		||||
import { navigate } from "../../common/navigate";
 | 
			
		||||
import "../../components/ha-dialog";
 | 
			
		||||
import "../../components/ha-header-bar";
 | 
			
		||||
@@ -29,12 +34,39 @@ import { haStyleDialog } from "../../resources/styles";
 | 
			
		||||
import "../../state-summary/state-card-content";
 | 
			
		||||
import { HomeAssistant } from "../../types";
 | 
			
		||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
 | 
			
		||||
import "./more-info-content";
 | 
			
		||||
import "./ha-more-info-history";
 | 
			
		||||
import "./ha-more-info-logbook";
 | 
			
		||||
 | 
			
		||||
const DOMAINS_NO_INFO = ["camera", "configurator"];
 | 
			
		||||
const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
 | 
			
		||||
const EDITABLE_DOMAINS = ["script"];
 | 
			
		||||
 | 
			
		||||
const MORE_INFO_CONTROL_IMPORT = {
 | 
			
		||||
  alarm_control_panel: () => import("./controls/more-info-alarm_control_panel"),
 | 
			
		||||
  automation: () => import("./controls/more-info-automation"),
 | 
			
		||||
  camera: () => import("./controls/more-info-camera"),
 | 
			
		||||
  climate: () => import("./controls/more-info-climate"),
 | 
			
		||||
  configurator: () => import("./controls/more-info-configurator"),
 | 
			
		||||
  counter: () => import("./controls/more-info-counter"),
 | 
			
		||||
  cover: () => import("./controls/more-info-cover"),
 | 
			
		||||
  fan: () => import("./controls/more-info-fan"),
 | 
			
		||||
  group: () => import("./controls/more-info-group"),
 | 
			
		||||
  humidifier: () => import("./controls/more-info-humidifier"),
 | 
			
		||||
  input_datetime: () => import("./controls/more-info-input_datetime"),
 | 
			
		||||
  light: () => import("./controls/more-info-light"),
 | 
			
		||||
  lock: () => import("./controls/more-info-lock"),
 | 
			
		||||
  media_player: () => import("./controls/more-info-media_player"),
 | 
			
		||||
  person: () => import("./controls/more-info-person"),
 | 
			
		||||
  script: () => import("./controls/more-info-script"),
 | 
			
		||||
  sun: () => import("./controls/more-info-sun"),
 | 
			
		||||
  timer: () => import("./controls/more-info-timer"),
 | 
			
		||||
  vacuum: () => import("./controls/more-info-vacuum"),
 | 
			
		||||
  water_heater: () => import("./controls/more-info-water_heater"),
 | 
			
		||||
  weather: () => import("./controls/more-info-weather"),
 | 
			
		||||
  hidden: () => {},
 | 
			
		||||
  default: () => import("./controls/more-info-default"),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface MoreInfoDialogParams {
 | 
			
		||||
  entityId: string | null;
 | 
			
		||||
}
 | 
			
		||||
@@ -47,6 +79,8 @@ export class MoreInfoDialog extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _entityId?: string | null;
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _moreInfoType?: string;
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _currTabIndex = 0;
 | 
			
		||||
 | 
			
		||||
  public showDialog(params: MoreInfoDialogParams) {
 | 
			
		||||
@@ -63,6 +97,23 @@ export class MoreInfoDialog extends LitElement {
 | 
			
		||||
    fireEvent(this, "dialog-closed", { dialog: this.localName });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected updated(changedProperties) {
 | 
			
		||||
    if (!this.hass || !this._entityId || !changedProperties.has("_entityId")) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const stateObj = this.hass.states[this._entityId];
 | 
			
		||||
    if (!stateObj) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (stateObj.attributes && "custom_ui_more_info" in stateObj.attributes) {
 | 
			
		||||
      this._moreInfoType = stateObj.attributes.custom_ui_more_info;
 | 
			
		||||
    } else {
 | 
			
		||||
      const type = stateMoreInfoType(stateObj);
 | 
			
		||||
      this._moreInfoType = `more-info-${type}`;
 | 
			
		||||
      MORE_INFO_CONTROL_IMPORT[type]();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    if (!this._entityId) {
 | 
			
		||||
      return html``;
 | 
			
		||||
@@ -127,7 +178,8 @@ export class MoreInfoDialog extends LitElement {
 | 
			
		||||
                `
 | 
			
		||||
              : ""}
 | 
			
		||||
          </ha-header-bar>
 | 
			
		||||
          ${this._computeShowHistoryComponent(entityId)
 | 
			
		||||
          ${DOMAINS_WITH_MORE_INFO.includes(domain) &&
 | 
			
		||||
          this._computeShowHistoryComponent(entityId)
 | 
			
		||||
            ? html`
 | 
			
		||||
                <mwc-tab-bar
 | 
			
		||||
                  .activeIndex=${this._currTabIndex}
 | 
			
		||||
@@ -135,7 +187,7 @@ export class MoreInfoDialog extends LitElement {
 | 
			
		||||
                >
 | 
			
		||||
                  <mwc-tab
 | 
			
		||||
                    .label=${this.hass.localize(
 | 
			
		||||
                      "ui.dialogs.more_info_control.controls"
 | 
			
		||||
                      "ui.dialogs.more_info_control.details"
 | 
			
		||||
                    )}
 | 
			
		||||
                  ></mwc-tab>
 | 
			
		||||
                  <mwc-tab
 | 
			
		||||
@@ -160,10 +212,23 @@ export class MoreInfoDialog extends LitElement {
 | 
			
		||||
                          .hass=${this.hass}
 | 
			
		||||
                        ></state-card-content>
 | 
			
		||||
                      `}
 | 
			
		||||
                  <more-info-content
 | 
			
		||||
                    .stateObj=${stateObj}
 | 
			
		||||
                    .hass=${this.hass}
 | 
			
		||||
                  ></more-info-content>
 | 
			
		||||
                  ${DOMAINS_WITH_MORE_INFO.includes(domain) ||
 | 
			
		||||
                  !this._computeShowHistoryComponent(entityId)
 | 
			
		||||
                    ? ""
 | 
			
		||||
                    : html`<ha-more-info-history
 | 
			
		||||
                          .hass=${this.hass}
 | 
			
		||||
                          .entityId=${this._entityId}
 | 
			
		||||
                        ></ha-more-info-history>
 | 
			
		||||
                        <ha-more-info-logbook
 | 
			
		||||
                          .hass=${this.hass}
 | 
			
		||||
                          .entityId=${this._entityId}
 | 
			
		||||
                        ></ha-more-info-logbook>`}
 | 
			
		||||
                  ${this._moreInfoType
 | 
			
		||||
                    ? dynamicElement(this._moreInfoType, {
 | 
			
		||||
                        hass: this.hass,
 | 
			
		||||
                        stateObj,
 | 
			
		||||
                      })
 | 
			
		||||
                    : ""}
 | 
			
		||||
                  ${stateObj.attributes.restored
 | 
			
		||||
                    ? html`
 | 
			
		||||
                        <p>
 | 
			
		||||
@@ -188,10 +253,14 @@ export class MoreInfoDialog extends LitElement {
 | 
			
		||||
                    : ""}
 | 
			
		||||
                `
 | 
			
		||||
              : html`
 | 
			
		||||
                  <ha-more-info-tab-history
 | 
			
		||||
                  <ha-more-info-history
 | 
			
		||||
                    .hass=${this.hass}
 | 
			
		||||
                    .entityId=${this._entityId}
 | 
			
		||||
                  ></ha-more-info-tab-history>
 | 
			
		||||
                  ></ha-more-info-history>
 | 
			
		||||
                  <ha-more-info-logbook
 | 
			
		||||
                    .hass=${this.hass}
 | 
			
		||||
                    .entityId=${this._entityId}
 | 
			
		||||
                  ></ha-more-info-logbook>
 | 
			
		||||
                `
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -199,17 +268,14 @@ export class MoreInfoDialog extends LitElement {
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected firstUpdated(): void {
 | 
			
		||||
    import("./ha-more-info-tab-history");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _enlarge() {
 | 
			
		||||
    this.large = !this.large;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _computeShowHistoryComponent(entityId) {
 | 
			
		||||
    return (
 | 
			
		||||
      isComponentLoaded(this.hass, "history") &&
 | 
			
		||||
      (isComponentLoaded(this.hass, "history") ||
 | 
			
		||||
        isComponentLoaded(this.hass, "logbook")) &&
 | 
			
		||||
      !DOMAINS_MORE_INFO_NO_HISTORY.includes(computeDomain(entityId))
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
@@ -274,6 +340,7 @@ export class MoreInfoDialog extends LitElement {
 | 
			
		||||
          --mdc-theme-on-primary: var(--primary-text-color);
 | 
			
		||||
          --mdc-theme-primary: var(--mdc-theme-surface);
 | 
			
		||||
          flex-shrink: 0;
 | 
			
		||||
          display: block;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @media all and (max-width: 450px), all and (max-height: 500px) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										109
									
								
								src/dialogs/more-info/ha-more-info-history.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/dialogs/more-info/ha-more-info-history.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
import {
 | 
			
		||||
  css,
 | 
			
		||||
  customElement,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  PropertyValues,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
 | 
			
		||||
import { throttle } from "../../common/util/throttle";
 | 
			
		||||
import "../../components/state-history-charts";
 | 
			
		||||
import { getRecentWithCache } from "../../data/cached-history";
 | 
			
		||||
import { HistoryResult } from "../../data/history";
 | 
			
		||||
import { haStyle } from "../../resources/styles";
 | 
			
		||||
import { HomeAssistant } from "../../types";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-more-info-history")
 | 
			
		||||
export class MoreInfoHistory extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property() public entityId!: string;
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _stateHistory?: HistoryResult;
 | 
			
		||||
 | 
			
		||||
  private _throttleGetStateHistory = throttle(() => {
 | 
			
		||||
    this._getStateHistory();
 | 
			
		||||
  }, 10000);
 | 
			
		||||
 | 
			
		||||
  protected render(): TemplateResult {
 | 
			
		||||
    if (!this.entityId) {
 | 
			
		||||
      return html``;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return html`${isComponentLoaded(this.hass, "history")
 | 
			
		||||
      ? html`<state-history-charts
 | 
			
		||||
          up-to-now
 | 
			
		||||
          .hass=${this.hass}
 | 
			
		||||
          .historyData=${this._stateHistory}
 | 
			
		||||
          .isLoadingData=${!this._stateHistory}
 | 
			
		||||
        ></state-history-charts>`
 | 
			
		||||
      : ""} `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected updated(changedProps: PropertyValues): void {
 | 
			
		||||
    super.updated(changedProps);
 | 
			
		||||
 | 
			
		||||
    if (changedProps.has("entityId")) {
 | 
			
		||||
      this._stateHistory = undefined;
 | 
			
		||||
 | 
			
		||||
      if (!this.entityId) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this._throttleGetStateHistory();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this.entityId || !changedProps.has("hass")) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      oldHass &&
 | 
			
		||||
      this.hass.states[this.entityId] !== oldHass?.states[this.entityId]
 | 
			
		||||
    ) {
 | 
			
		||||
      // wait for commit of data (we only account for the default setting of 1 sec)
 | 
			
		||||
      setTimeout(this._throttleGetStateHistory, 1000);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _getStateHistory(): Promise<void> {
 | 
			
		||||
    if (!isComponentLoaded(this.hass, "history")) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this._stateHistory = await getRecentWithCache(
 | 
			
		||||
      this.hass!,
 | 
			
		||||
      this.entityId,
 | 
			
		||||
      {
 | 
			
		||||
        refresh: 60,
 | 
			
		||||
        cacheKey: `more_info.${this.entityId}`,
 | 
			
		||||
        hoursToShow: 24,
 | 
			
		||||
      },
 | 
			
		||||
      this.hass!.localize,
 | 
			
		||||
      this.hass!.language
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get styles() {
 | 
			
		||||
    return [
 | 
			
		||||
      haStyle,
 | 
			
		||||
      css`
 | 
			
		||||
        state-history-charts {
 | 
			
		||||
          display: block;
 | 
			
		||||
          margin-bottom: 16px;
 | 
			
		||||
        }
 | 
			
		||||
      `,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-more-info-history": MoreInfoHistory;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										171
									
								
								src/dialogs/more-info/ha-more-info-logbook.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								src/dialogs/more-info/ha-more-info-logbook.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,171 @@
 | 
			
		||||
import {
 | 
			
		||||
  css,
 | 
			
		||||
  customElement,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  PropertyValues,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
 | 
			
		||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
 | 
			
		||||
import { throttle } from "../../common/util/throttle";
 | 
			
		||||
import "../../components/ha-circular-progress";
 | 
			
		||||
import "../../components/state-history-charts";
 | 
			
		||||
import { getLogbookData, LogbookEntry } from "../../data/logbook";
 | 
			
		||||
import "../../panels/logbook/ha-logbook";
 | 
			
		||||
import { haStyle, haStyleScrollbar } from "../../resources/styles";
 | 
			
		||||
import { HomeAssistant } from "../../types";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-more-info-logbook")
 | 
			
		||||
export class MoreInfoLogbook extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property() public entityId!: string;
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _logbookEntries?: LogbookEntry[];
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _persons = {};
 | 
			
		||||
 | 
			
		||||
  private _lastLogbookDate?: Date;
 | 
			
		||||
 | 
			
		||||
  private _throttleGetLogbookEntries = throttle(() => {
 | 
			
		||||
    this._getLogBookData();
 | 
			
		||||
  }, 10000);
 | 
			
		||||
 | 
			
		||||
  protected render(): TemplateResult {
 | 
			
		||||
    if (!this.entityId) {
 | 
			
		||||
      return html``;
 | 
			
		||||
    }
 | 
			
		||||
    const stateObj = this.hass.states[this.entityId];
 | 
			
		||||
 | 
			
		||||
    if (!stateObj) {
 | 
			
		||||
      return html``;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      ${isComponentLoaded(this.hass, "logbook")
 | 
			
		||||
        ? !this._logbookEntries
 | 
			
		||||
          ? html`
 | 
			
		||||
              <ha-circular-progress
 | 
			
		||||
                active
 | 
			
		||||
                alt=${this.hass.localize("ui.common.loading")}
 | 
			
		||||
              ></ha-circular-progress>
 | 
			
		||||
            `
 | 
			
		||||
          : this._logbookEntries.length
 | 
			
		||||
          ? html`
 | 
			
		||||
              <ha-logbook
 | 
			
		||||
                class="ha-scrollbar"
 | 
			
		||||
                narrow
 | 
			
		||||
                no-icon
 | 
			
		||||
                no-name
 | 
			
		||||
                .hass=${this.hass}
 | 
			
		||||
                .entries=${this._logbookEntries}
 | 
			
		||||
                .userIdToName=${this._persons}
 | 
			
		||||
              ></ha-logbook>
 | 
			
		||||
            `
 | 
			
		||||
          : html`<div class="no-entries">
 | 
			
		||||
              ${this.hass.localize("ui.components.logbook.entries_not_found")}
 | 
			
		||||
            </div>`
 | 
			
		||||
        : ""}
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected firstUpdated(): void {
 | 
			
		||||
    this._fetchPersonNames();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected updated(changedProps: PropertyValues): void {
 | 
			
		||||
    super.updated(changedProps);
 | 
			
		||||
 | 
			
		||||
    if (changedProps.has("entityId")) {
 | 
			
		||||
      this._lastLogbookDate = undefined;
 | 
			
		||||
      this._logbookEntries = undefined;
 | 
			
		||||
 | 
			
		||||
      if (!this.entityId) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this._throttleGetLogbookEntries();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this.entityId || !changedProps.has("hass")) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      oldHass &&
 | 
			
		||||
      this.hass.states[this.entityId] !== oldHass?.states[this.entityId]
 | 
			
		||||
    ) {
 | 
			
		||||
      // wait for commit of data (we only account for the default setting of 1 sec)
 | 
			
		||||
      setTimeout(this._throttleGetLogbookEntries, 1000);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _getLogBookData() {
 | 
			
		||||
    if (!isComponentLoaded(this.hass, "logbook")) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const lastDate =
 | 
			
		||||
      this._lastLogbookDate ||
 | 
			
		||||
      new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
 | 
			
		||||
    const now = new Date();
 | 
			
		||||
    const newEntries = await getLogbookData(
 | 
			
		||||
      this.hass,
 | 
			
		||||
      lastDate.toISOString(),
 | 
			
		||||
      now.toISOString(),
 | 
			
		||||
      this.entityId,
 | 
			
		||||
      true
 | 
			
		||||
    );
 | 
			
		||||
    this._logbookEntries = this._logbookEntries
 | 
			
		||||
      ? [...newEntries, ...this._logbookEntries]
 | 
			
		||||
      : newEntries;
 | 
			
		||||
    this._lastLogbookDate = now;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _fetchPersonNames() {
 | 
			
		||||
    Object.values(this.hass.states).forEach((entity) => {
 | 
			
		||||
      if (
 | 
			
		||||
        entity.attributes.user_id &&
 | 
			
		||||
        computeStateDomain(entity) === "person"
 | 
			
		||||
      ) {
 | 
			
		||||
        this._persons[entity.attributes.user_id] =
 | 
			
		||||
          entity.attributes.friendly_name;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get styles() {
 | 
			
		||||
    return [
 | 
			
		||||
      haStyle,
 | 
			
		||||
      haStyleScrollbar,
 | 
			
		||||
      css`
 | 
			
		||||
        .no-entries {
 | 
			
		||||
          text-align: center;
 | 
			
		||||
          padding: 16px;
 | 
			
		||||
          color: var(--secondary-text-color);
 | 
			
		||||
        }
 | 
			
		||||
        ha-logbook {
 | 
			
		||||
          max-height: 250px;
 | 
			
		||||
          overflow: auto;
 | 
			
		||||
          display: block;
 | 
			
		||||
          margin-top: 16px;
 | 
			
		||||
        }
 | 
			
		||||
        ha-circular-progress {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          justify-content: center;
 | 
			
		||||
        }
 | 
			
		||||
      `,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-more-info-logbook": MoreInfoLogbook;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,166 +0,0 @@
 | 
			
		||||
import {
 | 
			
		||||
  css,
 | 
			
		||||
  customElement,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  PropertyValues,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { styleMap } from "lit-html/directives/style-map";
 | 
			
		||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
 | 
			
		||||
import "../../components/ha-circular-progress";
 | 
			
		||||
import "../../components/state-history-charts";
 | 
			
		||||
import { getRecentWithCache } from "../../data/cached-history";
 | 
			
		||||
import { HistoryResult } from "../../data/history";
 | 
			
		||||
import { getLogbookData, LogbookEntry } from "../../data/logbook";
 | 
			
		||||
import "../../panels/logbook/ha-logbook";
 | 
			
		||||
import { haStyleDialog } from "../../resources/styles";
 | 
			
		||||
import { HomeAssistant } from "../../types";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-more-info-tab-history")
 | 
			
		||||
export class MoreInfoTabHistoryDialog extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property() public entityId!: string;
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _stateHistory?: HistoryResult;
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _entries?: LogbookEntry[];
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _persons = {};
 | 
			
		||||
 | 
			
		||||
  private _historyRefreshInterval?: number;
 | 
			
		||||
 | 
			
		||||
  protected render(): TemplateResult {
 | 
			
		||||
    if (!this.entityId) {
 | 
			
		||||
      return html``;
 | 
			
		||||
    }
 | 
			
		||||
    const stateObj = this.hass.states[this.entityId];
 | 
			
		||||
 | 
			
		||||
    if (!stateObj) {
 | 
			
		||||
      return html``;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <state-history-charts
 | 
			
		||||
        up-to-now
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        .historyData=${this._stateHistory}
 | 
			
		||||
        .isLoadingData=${!this._stateHistory}
 | 
			
		||||
      ></state-history-charts>
 | 
			
		||||
      ${!this._entries
 | 
			
		||||
        ? html`
 | 
			
		||||
            <ha-circular-progress
 | 
			
		||||
              active
 | 
			
		||||
              alt=${this.hass.localize("ui.common.loading")}
 | 
			
		||||
            ></ha-circular-progress>
 | 
			
		||||
          `
 | 
			
		||||
        : this._entries.length
 | 
			
		||||
        ? html`
 | 
			
		||||
            <ha-logbook
 | 
			
		||||
              narrow
 | 
			
		||||
              no-icon
 | 
			
		||||
              no-name
 | 
			
		||||
              style=${styleMap({
 | 
			
		||||
                height: `${(this._entries.length + 1) * 56}px`,
 | 
			
		||||
              })}
 | 
			
		||||
              .hass=${this.hass}
 | 
			
		||||
              .entries=${this._entries}
 | 
			
		||||
              .userIdToName=${this._persons}
 | 
			
		||||
            ></ha-logbook>
 | 
			
		||||
          `
 | 
			
		||||
        : ""}
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected firstUpdated(): void {
 | 
			
		||||
    this._fetchPersonNames();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected updated(changedProps: PropertyValues): void {
 | 
			
		||||
    super.updated(changedProps);
 | 
			
		||||
    if (!this.entityId) {
 | 
			
		||||
      clearInterval(this._historyRefreshInterval);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (changedProps.has("entityId")) {
 | 
			
		||||
      this._stateHistory = undefined;
 | 
			
		||||
      this._entries = undefined;
 | 
			
		||||
 | 
			
		||||
      this._getStateHistory();
 | 
			
		||||
      this._getLogBookData();
 | 
			
		||||
 | 
			
		||||
      clearInterval(this._historyRefreshInterval);
 | 
			
		||||
      this._historyRefreshInterval = window.setInterval(() => {
 | 
			
		||||
        this._getStateHistory();
 | 
			
		||||
      }, 60 * 1000);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _getStateHistory(): Promise<void> {
 | 
			
		||||
    this._stateHistory = await getRecentWithCache(
 | 
			
		||||
      this.hass!,
 | 
			
		||||
      this.entityId,
 | 
			
		||||
      {
 | 
			
		||||
        refresh: 60,
 | 
			
		||||
        cacheKey: `more_info.${this.entityId}`,
 | 
			
		||||
        hoursToShow: 24,
 | 
			
		||||
      },
 | 
			
		||||
      this.hass!.localize,
 | 
			
		||||
      this.hass!.language
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _getLogBookData() {
 | 
			
		||||
    const yesterday = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
 | 
			
		||||
    const now = new Date();
 | 
			
		||||
    this._entries = await getLogbookData(
 | 
			
		||||
      this.hass,
 | 
			
		||||
      yesterday.toISOString(),
 | 
			
		||||
      now.toISOString(),
 | 
			
		||||
      this.entityId,
 | 
			
		||||
      true
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _fetchPersonNames() {
 | 
			
		||||
    Object.values(this.hass.states).forEach((entity) => {
 | 
			
		||||
      if (
 | 
			
		||||
        entity.attributes.user_id &&
 | 
			
		||||
        computeStateDomain(entity) === "person"
 | 
			
		||||
      ) {
 | 
			
		||||
        this._persons[entity.attributes.user_id] =
 | 
			
		||||
          entity.attributes.friendly_name;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get styles() {
 | 
			
		||||
    return [
 | 
			
		||||
      haStyleDialog,
 | 
			
		||||
      css`
 | 
			
		||||
        state-history-charts {
 | 
			
		||||
          display: block;
 | 
			
		||||
          margin-bottom: 16px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ha-logbook {
 | 
			
		||||
          max-height: 360px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ha-circular-progress {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          justify-content: center;
 | 
			
		||||
        }
 | 
			
		||||
      `,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-more-info-tab-history": MoreInfoTabHistoryDialog;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,73 +0,0 @@
 | 
			
		||||
import { HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
import { property, PropertyValues, UpdatingElement } from "lit-element";
 | 
			
		||||
import dynamicContentUpdater from "../../common/dom/dynamic_content_updater";
 | 
			
		||||
import { stateMoreInfoType } from "../../common/entity/state_more_info_type";
 | 
			
		||||
import { HomeAssistant } from "../../types";
 | 
			
		||||
import "./controls/more-info-alarm_control_panel";
 | 
			
		||||
import "./controls/more-info-automation";
 | 
			
		||||
import "./controls/more-info-camera";
 | 
			
		||||
import "./controls/more-info-climate";
 | 
			
		||||
import "./controls/more-info-configurator";
 | 
			
		||||
import "./controls/more-info-counter";
 | 
			
		||||
import "./controls/more-info-cover";
 | 
			
		||||
import "./controls/more-info-default";
 | 
			
		||||
import "./controls/more-info-fan";
 | 
			
		||||
import "./controls/more-info-group";
 | 
			
		||||
import "./controls/more-info-humidifier";
 | 
			
		||||
import "./controls/more-info-input_datetime";
 | 
			
		||||
import "./controls/more-info-light";
 | 
			
		||||
import "./controls/more-info-lock";
 | 
			
		||||
import "./controls/more-info-media_player";
 | 
			
		||||
import "./controls/more-info-person";
 | 
			
		||||
import "./controls/more-info-script";
 | 
			
		||||
import "./controls/more-info-sun";
 | 
			
		||||
import "./controls/more-info-timer";
 | 
			
		||||
import "./controls/more-info-vacuum";
 | 
			
		||||
import "./controls/more-info-water_heater";
 | 
			
		||||
import "./controls/more-info-weather";
 | 
			
		||||
 | 
			
		||||
class MoreInfoContent extends UpdatingElement {
 | 
			
		||||
  @property({ attribute: false }) public hass?: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property() public stateObj?: HassEntity;
 | 
			
		||||
 | 
			
		||||
  private _detachedChild?: ChildNode;
 | 
			
		||||
 | 
			
		||||
  protected firstUpdated(): void {
 | 
			
		||||
    this.style.position = "relative";
 | 
			
		||||
    this.style.display = "block";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // This is not a lit element, but an updating element, so we implement update
 | 
			
		||||
  protected update(changedProps: PropertyValues): void {
 | 
			
		||||
    super.update(changedProps);
 | 
			
		||||
    const stateObj = this.stateObj;
 | 
			
		||||
    const hass = this.hass;
 | 
			
		||||
 | 
			
		||||
    if (!stateObj || !hass) {
 | 
			
		||||
      if (this.lastChild) {
 | 
			
		||||
        this._detachedChild = this.lastChild;
 | 
			
		||||
        // Detach child to prevent it from doing work.
 | 
			
		||||
        this.removeChild(this.lastChild);
 | 
			
		||||
      }
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this._detachedChild) {
 | 
			
		||||
      this.appendChild(this._detachedChild);
 | 
			
		||||
      this._detachedChild = undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const moreInfoType =
 | 
			
		||||
      stateObj.attributes && "custom_ui_more_info" in stateObj.attributes
 | 
			
		||||
        ? stateObj.attributes.custom_ui_more_info
 | 
			
		||||
        : "more-info-" + stateMoreInfoType(stateObj);
 | 
			
		||||
 | 
			
		||||
    dynamicContentUpdater(this, moreInfoType.toUpperCase(), {
 | 
			
		||||
      hass,
 | 
			
		||||
      stateObj,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define("more-info-content", MoreInfoContent);
 | 
			
		||||
@@ -7,5 +7,3 @@ import "../util/legacy-support";
 | 
			
		||||
setPassiveTouchGestures(true);
 | 
			
		||||
 | 
			
		||||
(window as any).frontendVersion = __VERSION__;
 | 
			
		||||
 | 
			
		||||
import("../resources/html-import/polyfill");
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@
 | 
			
		||||
      }
 | 
			
		||||
      @media (prefers-color-scheme: dark) {
 | 
			
		||||
        html {
 | 
			
		||||
          background-color: var(--primary-background-color, #111111);
 | 
			
		||||
          background-color: #111111;
 | 
			
		||||
        }
 | 
			
		||||
        #ha-init-skeleton::before {
 | 
			
		||||
          background-color: #1c1c1c;
 | 
			
		||||
@@ -100,9 +100,5 @@
 | 
			
		||||
        {% endfor -%}
 | 
			
		||||
      }
 | 
			
		||||
    </script>
 | 
			
		||||
 | 
			
		||||
    {% for extra_url in extra_urls -%}
 | 
			
		||||
    <link rel="import" href="{{ extra_url }}" async />
 | 
			
		||||
    {% endfor -%}
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,20 @@
 | 
			
		||||
    <link rel="preload" href="<%= latestPageJS %>" as="script" crossorigin="use-credentials" />
 | 
			
		||||
    <%= renderTemplate('_header') %>
 | 
			
		||||
    <style>
 | 
			
		||||
      html {
 | 
			
		||||
        color: var(--primary-text-color, #212121);
 | 
			
		||||
      }
 | 
			
		||||
      @media (prefers-color-scheme: dark) {
 | 
			
		||||
        html {
 | 
			
		||||
          background-color: #111111;
 | 
			
		||||
          color: #e1e1e1;
 | 
			
		||||
        }
 | 
			
		||||
        ha-onboarding {
 | 
			
		||||
          --primary-text-color: #e1e1e1;
 | 
			
		||||
          --secondary-text-color: #9b9b9b;
 | 
			
		||||
          --disabled-text-color: #6f6f6f;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      .content {
 | 
			
		||||
        padding: 20px 16px;
 | 
			
		||||
        max-width: 400px;
 | 
			
		||||
@@ -23,14 +37,6 @@
 | 
			
		||||
      .header img {
 | 
			
		||||
        margin-right: 16px;
 | 
			
		||||
      }
 | 
			
		||||
      @media (prefers-color-scheme: dark) {
 | 
			
		||||
        body {
 | 
			
		||||
          background-color: #111111;
 | 
			
		||||
          color: #e1e1e1;
 | 
			
		||||
          --primary-text-color: #e1e1e1;
 | 
			
		||||
          --secondary-text-color: #9b9b9b;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    </style>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
 
 | 
			
		||||
@@ -63,6 +63,7 @@ class HassErrorScreen extends LitElement {
 | 
			
		||||
          pointer-events: auto;
 | 
			
		||||
        }
 | 
			
		||||
        .content {
 | 
			
		||||
          color: var(--primary-text-color);
 | 
			
		||||
          height: calc(100% - 64px);
 | 
			
		||||
          display: flex;
 | 
			
		||||
          align-items: center;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,26 +3,26 @@ import {
 | 
			
		||||
  css,
 | 
			
		||||
  CSSResult,
 | 
			
		||||
  customElement,
 | 
			
		||||
  eventOptions,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  PropertyValues,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
  eventOptions,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { classMap } from "lit-html/directives/class-map";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
 | 
			
		||||
import { navigate } from "../common/navigate";
 | 
			
		||||
import "../components/ha-menu-button";
 | 
			
		||||
import "../components/ha-icon-button-arrow-prev";
 | 
			
		||||
import { HomeAssistant, Route } from "../types";
 | 
			
		||||
import "../components/ha-svg-icon";
 | 
			
		||||
import "../components/ha-icon";
 | 
			
		||||
import "../components/ha-tab";
 | 
			
		||||
import { restoreScroll } from "../common/decorators/restore-scroll";
 | 
			
		||||
import { navigate } from "../common/navigate";
 | 
			
		||||
import { computeRTL } from "../common/util/compute_rtl";
 | 
			
		||||
import "../components/ha-icon";
 | 
			
		||||
import "../components/ha-icon-button-arrow-prev";
 | 
			
		||||
import "../components/ha-menu-button";
 | 
			
		||||
import "../components/ha-svg-icon";
 | 
			
		||||
import "../components/ha-tab";
 | 
			
		||||
import { HomeAssistant, Route } from "../types";
 | 
			
		||||
 | 
			
		||||
export interface PageNavigation {
 | 
			
		||||
  path: string;
 | 
			
		||||
@@ -132,7 +132,7 @@ class HassTabsSubpage extends LitElement {
 | 
			
		||||
      this.hass.language,
 | 
			
		||||
      this.narrow
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const showTabs = tabs.length > 1 || !this.narrow;
 | 
			
		||||
    return html`
 | 
			
		||||
      <div class="toolbar">
 | 
			
		||||
        ${this.mainPage
 | 
			
		||||
@@ -152,7 +152,7 @@ class HassTabsSubpage extends LitElement {
 | 
			
		||||
        ${this.narrow
 | 
			
		||||
          ? html` <div class="main-title"><slot name="header"></slot></div> `
 | 
			
		||||
          : ""}
 | 
			
		||||
        ${tabs.length > 1 || !this.narrow
 | 
			
		||||
        ${showTabs
 | 
			
		||||
          ? html`
 | 
			
		||||
              <div id="tabbar" class=${classMap({ "bottom-bar": this.narrow })}>
 | 
			
		||||
                ${tabs}
 | 
			
		||||
@@ -163,10 +163,15 @@ class HassTabsSubpage extends LitElement {
 | 
			
		||||
          <slot name="toolbar-icon"></slot>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="content" @scroll=${this._saveScrollPos}>
 | 
			
		||||
      <div
 | 
			
		||||
        class="content ${classMap({ tabs: showTabs })}"
 | 
			
		||||
        @scroll=${this._saveScrollPos}
 | 
			
		||||
      >
 | 
			
		||||
        <slot></slot>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div id="fab"><slot name="fab"></slot></div>
 | 
			
		||||
      <div id="fab" class="${classMap({ tabs: showTabs })}">
 | 
			
		||||
        <slot name="fab"></slot>
 | 
			
		||||
      </div>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -274,12 +279,13 @@ class HassTabsSubpage extends LitElement {
 | 
			
		||||
        margin-left: env(safe-area-inset-left);
 | 
			
		||||
        margin-right: env(safe-area-inset-right);
 | 
			
		||||
        height: calc(100% - 65px);
 | 
			
		||||
        height: calc(100% - 65px - env(safe-area-inset-bottom));
 | 
			
		||||
        overflow-y: auto;
 | 
			
		||||
        overflow: auto;
 | 
			
		||||
        -webkit-overflow-scrolling: touch;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      :host([narrow]) .content {
 | 
			
		||||
      :host([narrow]) .content.tabs {
 | 
			
		||||
        height: calc(100% - 128px);
 | 
			
		||||
        height: calc(100% - 128px - env(safe-area-inset-bottom));
 | 
			
		||||
      }
 | 
			
		||||
@@ -290,7 +296,7 @@ class HassTabsSubpage extends LitElement {
 | 
			
		||||
        bottom: calc(16px + env(safe-area-inset-bottom));
 | 
			
		||||
        z-index: 1;
 | 
			
		||||
      }
 | 
			
		||||
      :host([narrow]) #fab {
 | 
			
		||||
      :host([narrow]) #fab.tabs {
 | 
			
		||||
        bottom: calc(84px + env(safe-area-inset-bottom));
 | 
			
		||||
      }
 | 
			
		||||
      #fab[is-wide] {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ const NON_SWIPABLE_PANELS = ["map"];
 | 
			
		||||
declare global {
 | 
			
		||||
  // for fire event
 | 
			
		||||
  interface HASSDomEvents {
 | 
			
		||||
    "hass-open-menu": undefined;
 | 
			
		||||
    "hass-toggle-menu": undefined;
 | 
			
		||||
    "hass-show-notifications": undefined;
 | 
			
		||||
  }
 | 
			
		||||
@@ -92,6 +93,17 @@ class HomeAssistantMain extends LitElement {
 | 
			
		||||
  protected firstUpdated() {
 | 
			
		||||
    import(/* webpackChunkName: "ha-sidebar" */ "../components/ha-sidebar");
 | 
			
		||||
 | 
			
		||||
    this.addEventListener("hass-open-menu", () => {
 | 
			
		||||
      if (this._sidebarNarrow) {
 | 
			
		||||
        this.drawer.open();
 | 
			
		||||
      } else {
 | 
			
		||||
        fireEvent(this, "hass-dock-sidebar", {
 | 
			
		||||
          dock: "docked",
 | 
			
		||||
        });
 | 
			
		||||
        setTimeout(() => this.appLayout.resetLayout());
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.addEventListener("hass-toggle-menu", () => {
 | 
			
		||||
      if (this._sidebarNarrow) {
 | 
			
		||||
        if (this.drawer.opened) {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,9 +6,9 @@ import {
 | 
			
		||||
  CSSResult,
 | 
			
		||||
  customElement,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  PropertyValues,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,8 @@
 | 
			
		||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
 | 
			
		||||
import "@material/mwc-list/mwc-list-item";
 | 
			
		||||
import "@material/mwc-icon-button";
 | 
			
		||||
import "../../../../components/ha-button-menu";
 | 
			
		||||
import "../../../../components/ha-svg-icon";
 | 
			
		||||
import { mdiDotsVertical, mdiArrowUp, mdiArrowDown } from "@mdi/js";
 | 
			
		||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
 | 
			
		||||
import "@material/mwc-list/mwc-list-item";
 | 
			
		||||
import { mdiArrowDown, mdiArrowUp, mdiDotsVertical } from "@mdi/js";
 | 
			
		||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
 | 
			
		||||
import "@polymer/paper-item/paper-item";
 | 
			
		||||
import "@polymer/paper-listbox/paper-listbox";
 | 
			
		||||
import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
 | 
			
		||||
@@ -12,29 +11,31 @@ import {
 | 
			
		||||
  CSSResult,
 | 
			
		||||
  customElement,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  PropertyValues,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
 | 
			
		||||
import { fireEvent } from "../../../../common/dom/fire_event";
 | 
			
		||||
import "../../../../components/ha-button-menu";
 | 
			
		||||
import "../../../../components/ha-card";
 | 
			
		||||
import "../../../../components/ha-svg-icon";
 | 
			
		||||
import type { Action } from "../../../../data/script";
 | 
			
		||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
 | 
			
		||||
import { haStyle } from "../../../../resources/styles";
 | 
			
		||||
import type { HomeAssistant } from "../../../../types";
 | 
			
		||||
import { handleStructError } from "../../../lovelace/common/structs/handle-errors";
 | 
			
		||||
import "./types/ha-automation-action-choose";
 | 
			
		||||
import "./types/ha-automation-action-condition";
 | 
			
		||||
import "./types/ha-automation-action-delay";
 | 
			
		||||
import "./types/ha-automation-action-device_id";
 | 
			
		||||
import "./types/ha-automation-action-event";
 | 
			
		||||
import "./types/ha-automation-action-repeat";
 | 
			
		||||
import "./types/ha-automation-action-scene";
 | 
			
		||||
import "./types/ha-automation-action-service";
 | 
			
		||||
import "./types/ha-automation-action-wait_for_trigger";
 | 
			
		||||
import "./types/ha-automation-action-wait_template";
 | 
			
		||||
import "./types/ha-automation-action-repeat";
 | 
			
		||||
import "./types/ha-automation-action-choose";
 | 
			
		||||
import { handleStructError } from "../../../lovelace/common/structs/handle-errors";
 | 
			
		||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
 | 
			
		||||
import { haStyle } from "../../../../resources/styles";
 | 
			
		||||
 | 
			
		||||
const OPTIONS = [
 | 
			
		||||
  "condition",
 | 
			
		||||
@@ -44,6 +45,7 @@ const OPTIONS = [
 | 
			
		||||
  "scene",
 | 
			
		||||
  "service",
 | 
			
		||||
  "wait_template",
 | 
			
		||||
  "wait_for_trigger",
 | 
			
		||||
  "repeat",
 | 
			
		||||
  "choose",
 | 
			
		||||
];
 | 
			
		||||
@@ -166,12 +168,12 @@ export default class HaAutomationActionRow extends LitElement {
 | 
			
		||||
                      "ui.panel.config.automation.editor.edit_yaml"
 | 
			
		||||
                    )}
 | 
			
		||||
              </mwc-list-item>
 | 
			
		||||
              <mwc-list-item disabled>
 | 
			
		||||
              <mwc-list-item>
 | 
			
		||||
                ${this.hass.localize(
 | 
			
		||||
                  "ui.panel.config.automation.editor.actions.duplicate"
 | 
			
		||||
                )}
 | 
			
		||||
              </mwc-list-item>
 | 
			
		||||
              <mwc-list-item>
 | 
			
		||||
              <mwc-list-item class="warning">
 | 
			
		||||
                ${this.hass.localize(
 | 
			
		||||
                  "ui.panel.config.automation.editor.actions.delete"
 | 
			
		||||
                )}
 | 
			
		||||
@@ -261,6 +263,7 @@ export default class HaAutomationActionRow extends LitElement {
 | 
			
		||||
        this._switchYamlMode();
 | 
			
		||||
        break;
 | 
			
		||||
      case 1:
 | 
			
		||||
        fireEvent(this, "duplicate");
 | 
			
		||||
        break;
 | 
			
		||||
      case 2:
 | 
			
		||||
        this._onDelete();
 | 
			
		||||
@@ -333,7 +336,6 @@ export default class HaAutomationActionRow extends LitElement {
 | 
			
		||||
          --mdc-theme-text-primary-on-background: var(--disabled-text-color);
 | 
			
		||||
        }
 | 
			
		||||
        .warning {
 | 
			
		||||
          color: var(--warning-color);
 | 
			
		||||
          margin-bottom: 8px;
 | 
			
		||||
        }
 | 
			
		||||
        .warning ul {
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,7 @@ export default class HaAutomationAction extends LitElement {
 | 
			
		||||
            .index=${idx}
 | 
			
		||||
            .totalActions=${this.actions.length}
 | 
			
		||||
            .action=${action}
 | 
			
		||||
            @duplicate=${this._duplicateAction}
 | 
			
		||||
            @move-action=${this._move}
 | 
			
		||||
            @value-changed=${this._actionChanged}
 | 
			
		||||
            .hass=${this.hass}
 | 
			
		||||
@@ -78,6 +79,14 @@ export default class HaAutomationAction extends LitElement {
 | 
			
		||||
    fireEvent(this, "value-changed", { value: actions });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _duplicateAction(ev: CustomEvent) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    const index = (ev.target as any).index;
 | 
			
		||||
    fireEvent(this, "value-changed", {
 | 
			
		||||
      value: this.actions.concat(this.actions[index]),
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get styles(): CSSResult {
 | 
			
		||||
    return css`
 | 
			
		||||
      ha-automation-action-row,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,21 @@
 | 
			
		||||
import { mdiDelete } from "@mdi/js";
 | 
			
		||||
import "@polymer/paper-input/paper-input";
 | 
			
		||||
import "@polymer/paper-listbox/paper-listbox";
 | 
			
		||||
import {
 | 
			
		||||
  css,
 | 
			
		||||
  CSSResult,
 | 
			
		||||
  customElement,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  CSSResult,
 | 
			
		||||
  css,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { html } from "lit-html";
 | 
			
		||||
import { Action, ChooseAction } from "../../../../../data/script";
 | 
			
		||||
import { HomeAssistant } from "../../../../../types";
 | 
			
		||||
import { ActionElement } from "../ha-automation-action-row";
 | 
			
		||||
import "../../condition/ha-automation-condition-editor";
 | 
			
		||||
import "@polymer/paper-listbox/paper-listbox";
 | 
			
		||||
import { fireEvent } from "../../../../../common/dom/fire_event";
 | 
			
		||||
import "../ha-automation-action";
 | 
			
		||||
import { Condition } from "../../../../../data/automation";
 | 
			
		||||
import { Action, ChooseAction } from "../../../../../data/script";
 | 
			
		||||
import { haStyle } from "../../../../../resources/styles";
 | 
			
		||||
import { mdiDelete } from "@mdi/js";
 | 
			
		||||
import { HomeAssistant } from "../../../../../types";
 | 
			
		||||
import "../ha-automation-action";
 | 
			
		||||
import { ActionElement } from "../ha-automation-action-row";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-automation-action-choose")
 | 
			
		||||
export class HaChooseAction extends LitElement implements ActionElement {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,21 @@
 | 
			
		||||
import "@polymer/paper-input/paper-input";
 | 
			
		||||
import { customElement, LitElement, property, CSSResult } from "lit-element";
 | 
			
		||||
import { html } from "lit-html";
 | 
			
		||||
import {
 | 
			
		||||
  RepeatAction,
 | 
			
		||||
  Action,
 | 
			
		||||
  CountRepeat,
 | 
			
		||||
  WhileRepeat,
 | 
			
		||||
  UntilRepeat,
 | 
			
		||||
} from "../../../../../data/script";
 | 
			
		||||
import { HomeAssistant } from "../../../../../types";
 | 
			
		||||
import { ActionElement } from "../ha-automation-action-row";
 | 
			
		||||
import "../../condition/ha-automation-condition-editor";
 | 
			
		||||
import type { PaperListboxElement } from "@polymer/paper-listbox";
 | 
			
		||||
import "@polymer/paper-listbox/paper-listbox";
 | 
			
		||||
import { CSSResult, customElement, LitElement, property } from "lit-element";
 | 
			
		||||
import { html } from "lit-html";
 | 
			
		||||
import { fireEvent } from "../../../../../common/dom/fire_event";
 | 
			
		||||
import "../ha-automation-action";
 | 
			
		||||
import { Condition } from "../../../../lovelace/common/validate-condition";
 | 
			
		||||
import {
 | 
			
		||||
  Action,
 | 
			
		||||
  CountRepeat,
 | 
			
		||||
  RepeatAction,
 | 
			
		||||
  UntilRepeat,
 | 
			
		||||
  WhileRepeat,
 | 
			
		||||
} from "../../../../../data/script";
 | 
			
		||||
import { haStyle } from "../../../../../resources/styles";
 | 
			
		||||
import { HomeAssistant } from "../../../../../types";
 | 
			
		||||
import { Condition } from "../../../../lovelace/common/validate-condition";
 | 
			
		||||
import "../ha-automation-action";
 | 
			
		||||
import { ActionElement } from "../ha-automation-action-row";
 | 
			
		||||
 | 
			
		||||
const OPTIONS = ["count", "while", "until"];
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import {
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { html } from "lit-html";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { any, assert, object, optional, string } from "superstruct";
 | 
			
		||||
import { fireEvent } from "../../../../../common/dom/fire_event";
 | 
			
		||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
 | 
			
		||||
import { computeObjectId } from "../../../../../common/entity/compute_object_id";
 | 
			
		||||
@@ -18,14 +19,13 @@ import type { HaYamlEditor } from "../../../../../components/ha-yaml-editor";
 | 
			
		||||
import { ServiceAction } from "../../../../../data/script";
 | 
			
		||||
import type { PolymerChangedEvent } from "../../../../../polymer-types";
 | 
			
		||||
import type { HomeAssistant } from "../../../../../types";
 | 
			
		||||
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
 | 
			
		||||
import { assert, optional, object, string } from "superstruct";
 | 
			
		||||
import { EntityId } from "../../../../lovelace/common/structs/is-entity-id";
 | 
			
		||||
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
 | 
			
		||||
 | 
			
		||||
const actionStruct = object({
 | 
			
		||||
  service: optional(string()),
 | 
			
		||||
  entity_id: optional(EntityId),
 | 
			
		||||
  data: optional(object()),
 | 
			
		||||
  data: optional(any()),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@customElement("ha-automation-action-service")
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,70 @@
 | 
			
		||||
import "@polymer/paper-input/paper-input";
 | 
			
		||||
import "@polymer/paper-input/paper-textarea";
 | 
			
		||||
import { customElement, LitElement, property } from "lit-element";
 | 
			
		||||
import { html } from "lit-html";
 | 
			
		||||
import { fireEvent } from "../../../../../common/dom/fire_event";
 | 
			
		||||
import "../../../../../components/ha-formfield";
 | 
			
		||||
import { WaitForTriggerAction } from "../../../../../data/script";
 | 
			
		||||
import { HomeAssistant } from "../../../../../types";
 | 
			
		||||
import "../../trigger/ha-automation-trigger";
 | 
			
		||||
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-automation-action-wait_for_trigger")
 | 
			
		||||
export class HaWaitForTriggerAction extends LitElement
 | 
			
		||||
  implements ActionElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property() public action!: WaitForTriggerAction;
 | 
			
		||||
 | 
			
		||||
  public static get defaultConfig() {
 | 
			
		||||
    return { wait_for_trigger: [], timeout: "" };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    const { wait_for_trigger, continue_on_timeout, timeout } = this.action;
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <paper-input
 | 
			
		||||
        .label=${this.hass.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.actions.type.wait_for_trigger.timeout"
 | 
			
		||||
        )}
 | 
			
		||||
        .name=${"timeout"}
 | 
			
		||||
        .value=${timeout}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
      ></paper-input>
 | 
			
		||||
      <br />
 | 
			
		||||
      <ha-formfield
 | 
			
		||||
        .label=${this.hass.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.actions.type.wait_for_trigger.timeout"
 | 
			
		||||
        )}
 | 
			
		||||
      >
 | 
			
		||||
        <ha-switch
 | 
			
		||||
          .checked=${continue_on_timeout}
 | 
			
		||||
          @change=${this._continueChanged}
 | 
			
		||||
        ></ha-switch>
 | 
			
		||||
      </ha-formfield>
 | 
			
		||||
      <ha-automation-trigger
 | 
			
		||||
        .triggers=${wait_for_trigger}
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        .name=${"wait_for_trigger"}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
      ></ha-automation-trigger>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _continueChanged(ev) {
 | 
			
		||||
    fireEvent(this, "value-changed", {
 | 
			
		||||
      value: { ...this.action, continue_on_timeout: ev.target.checked },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _valueChanged(ev: CustomEvent): void {
 | 
			
		||||
    handleChangeEvent(this, ev);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-automation-action-wait_for_trigger": HaWaitForTriggerAction;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,6 +2,7 @@ import "@polymer/paper-input/paper-input";
 | 
			
		||||
import "@polymer/paper-input/paper-textarea";
 | 
			
		||||
import { customElement, LitElement, property } from "lit-element";
 | 
			
		||||
import { html } from "lit-html";
 | 
			
		||||
import { fireEvent } from "../../../../../common/dom/fire_event";
 | 
			
		||||
import { WaitAction } from "../../../../../data/script";
 | 
			
		||||
import { HomeAssistant } from "../../../../../types";
 | 
			
		||||
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
 | 
			
		||||
@@ -13,11 +14,11 @@ export class HaWaitAction extends LitElement implements ActionElement {
 | 
			
		||||
  @property() public action!: WaitAction;
 | 
			
		||||
 | 
			
		||||
  public static get defaultConfig() {
 | 
			
		||||
    return { wait_template: "", timeout: "" };
 | 
			
		||||
    return { wait_template: "" };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    const { wait_template, timeout } = this.action;
 | 
			
		||||
    const { wait_template, timeout, continue_on_timeout } = this.action;
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <paper-textarea
 | 
			
		||||
@@ -37,9 +38,24 @@ export class HaWaitAction extends LitElement implements ActionElement {
 | 
			
		||||
        .value=${timeout}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
      ></paper-input>
 | 
			
		||||
      <br />
 | 
			
		||||
      <ha-formfield
 | 
			
		||||
        .label=${this.hass.localize("ui.panel.config.automation.editor.actions.type.wait_template.continue_timeout")}
 | 
			
		||||
      >
 | 
			
		||||
        <ha-switch
 | 
			
		||||
          .checked=${continue_on_timeout}
 | 
			
		||||
          @change=${this._continueChanged}
 | 
			
		||||
        ></ha-switch>
 | 
			
		||||
      </ha-formfield>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _continueChanged(ev) {
 | 
			
		||||
    fireEvent(this, "value-changed", {
 | 
			
		||||
      value: { ...this.action, continue_on_timeout: ev.target.checked },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _valueChanged(ev: CustomEvent): void {
 | 
			
		||||
    handleChangeEvent(this, ev);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,25 @@
 | 
			
		||||
import "../../../../components/ha-icon-button";
 | 
			
		||||
import "@polymer/paper-item/paper-item";
 | 
			
		||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
 | 
			
		||||
import "@material/mwc-list/mwc-list-item";
 | 
			
		||||
import "../../../../components/ha-button-menu";
 | 
			
		||||
import { mdiDotsVertical } from "@mdi/js";
 | 
			
		||||
import "@polymer/paper-item/paper-item";
 | 
			
		||||
import {
 | 
			
		||||
  css,
 | 
			
		||||
  CSSResult,
 | 
			
		||||
  customElement,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { fireEvent } from "../../../../common/dom/fire_event";
 | 
			
		||||
import "../../../../components/ha-button-menu";
 | 
			
		||||
import "../../../../components/ha-card";
 | 
			
		||||
import "../../../../components/ha-icon-button";
 | 
			
		||||
import { Condition } from "../../../../data/automation";
 | 
			
		||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
 | 
			
		||||
import { HomeAssistant } from "../../../../types";
 | 
			
		||||
import "./ha-automation-condition-editor";
 | 
			
		||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
 | 
			
		||||
import { haStyle } from "../../../../resources/styles";
 | 
			
		||||
 | 
			
		||||
export interface ConditionElement extends LitElement {
 | 
			
		||||
  condition: Condition;
 | 
			
		||||
@@ -81,12 +82,12 @@ export default class HaAutomationConditionRow extends LitElement {
 | 
			
		||||
                      "ui.panel.config.automation.editor.edit_yaml"
 | 
			
		||||
                    )}
 | 
			
		||||
              </mwc-list-item>
 | 
			
		||||
              <mwc-list-item disabled>
 | 
			
		||||
              <mwc-list-item>
 | 
			
		||||
                ${this.hass.localize(
 | 
			
		||||
                  "ui.panel.config.automation.editor.actions.duplicate"
 | 
			
		||||
                )}
 | 
			
		||||
              </mwc-list-item>
 | 
			
		||||
              <mwc-list-item>
 | 
			
		||||
              <mwc-list-item class="warning">
 | 
			
		||||
                ${this.hass.localize(
 | 
			
		||||
                  "ui.panel.config.automation.editor.actions.delete"
 | 
			
		||||
                )}
 | 
			
		||||
@@ -109,6 +110,7 @@ export default class HaAutomationConditionRow extends LitElement {
 | 
			
		||||
        this._switchYamlMode();
 | 
			
		||||
        break;
 | 
			
		||||
      case 1:
 | 
			
		||||
        fireEvent(this, "duplicate");
 | 
			
		||||
        break;
 | 
			
		||||
      case 2:
 | 
			
		||||
        this._onDelete();
 | 
			
		||||
@@ -133,20 +135,23 @@ export default class HaAutomationConditionRow extends LitElement {
 | 
			
		||||
    this._yamlMode = !this._yamlMode;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get styles(): CSSResult {
 | 
			
		||||
    return css`
 | 
			
		||||
      .card-menu {
 | 
			
		||||
        float: right;
 | 
			
		||||
        z-index: 3;
 | 
			
		||||
        --mdc-theme-text-primary-on-background: var(--primary-text-color);
 | 
			
		||||
      }
 | 
			
		||||
      .rtl .card-menu {
 | 
			
		||||
        float: left;
 | 
			
		||||
      }
 | 
			
		||||
      mwc-list-item[disabled] {
 | 
			
		||||
        --mdc-theme-text-primary-on-background: var(--disabled-text-color);
 | 
			
		||||
      }
 | 
			
		||||
    `;
 | 
			
		||||
  static get styles(): CSSResult[] {
 | 
			
		||||
    return [
 | 
			
		||||
      haStyle,
 | 
			
		||||
      css`
 | 
			
		||||
        .card-menu {
 | 
			
		||||
          float: right;
 | 
			
		||||
          z-index: 3;
 | 
			
		||||
          --mdc-theme-text-primary-on-background: var(--primary-text-color);
 | 
			
		||||
        }
 | 
			
		||||
        .rtl .card-menu {
 | 
			
		||||
          float: left;
 | 
			
		||||
        }
 | 
			
		||||
        mwc-list-item[disabled] {
 | 
			
		||||
          --mdc-theme-text-primary-on-background: var(--disabled-text-color);
 | 
			
		||||
        }
 | 
			
		||||
      `,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import {
 | 
			
		||||
  html,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  PropertyValues,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { fireEvent } from "../../../../common/dom/fire_event";
 | 
			
		||||
import "../../../../components/ha-card";
 | 
			
		||||
@@ -20,13 +21,43 @@ export default class HaAutomationCondition extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @property() public conditions!: Condition[];
 | 
			
		||||
 | 
			
		||||
  protected updated(changedProperties: PropertyValues) {
 | 
			
		||||
    if (!changedProperties.has("conditions")) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    let updatedConditions: Condition[] | undefined;
 | 
			
		||||
    if (!Array.isArray(this.conditions)) {
 | 
			
		||||
      updatedConditions = [this.conditions];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    (updatedConditions || this.conditions).forEach((condition, index) => {
 | 
			
		||||
      if (typeof condition === "string") {
 | 
			
		||||
        updatedConditions = updatedConditions || [...this.conditions];
 | 
			
		||||
        updatedConditions[index] = {
 | 
			
		||||
          condition: "template",
 | 
			
		||||
          value_template: condition,
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (updatedConditions) {
 | 
			
		||||
      fireEvent(this, "value-changed", {
 | 
			
		||||
        value: updatedConditions,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    if (!Array.isArray(this.conditions)) {
 | 
			
		||||
      return html``;
 | 
			
		||||
    }
 | 
			
		||||
    return html`
 | 
			
		||||
      ${this.conditions.map(
 | 
			
		||||
        (cond, idx) => html`
 | 
			
		||||
          <ha-automation-condition-row
 | 
			
		||||
            .index=${idx}
 | 
			
		||||
            .condition=${cond}
 | 
			
		||||
            @duplicate=${this._duplicateCondition}
 | 
			
		||||
            @value-changed=${this._conditionChanged}
 | 
			
		||||
            .hass=${this.hass}
 | 
			
		||||
          ></ha-automation-condition-row>
 | 
			
		||||
@@ -68,6 +99,14 @@ export default class HaAutomationCondition extends LitElement {
 | 
			
		||||
    fireEvent(this, "value-changed", { value: conditions });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _duplicateCondition(ev: CustomEvent) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    const index = (ev.target as any).index;
 | 
			
		||||
    fireEvent(this, "value-changed", {
 | 
			
		||||
      value: this.conditions.concat(this.conditions[index]),
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get styles(): CSSResult {
 | 
			
		||||
    return css`
 | 
			
		||||
      ha-automation-condition-row,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import "@polymer/paper-input/paper-input";
 | 
			
		||||
import "@polymer/paper-input/paper-textarea";
 | 
			
		||||
import { customElement, html, LitElement, property } from "lit-element";
 | 
			
		||||
import { fireEvent } from "../../../../../common/dom/fire_event";
 | 
			
		||||
import "../../../../../components/entity/ha-entity-picker";
 | 
			
		||||
import { NumericStateCondition } from "../../../../../data/automation";
 | 
			
		||||
import { HomeAssistant } from "../../../../../types";
 | 
			
		||||
@@ -19,16 +18,34 @@ export default class HaNumericStateCondition extends LitElement {
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    const { value_template, entity_id, below, above } = this.condition;
 | 
			
		||||
  public render() {
 | 
			
		||||
    const {
 | 
			
		||||
      value_template,
 | 
			
		||||
      entity_id,
 | 
			
		||||
      attribute,
 | 
			
		||||
      below,
 | 
			
		||||
      above,
 | 
			
		||||
    } = this.condition;
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <ha-entity-picker
 | 
			
		||||
        .value="${entity_id}"
 | 
			
		||||
        @value-changed="${this._entityPicked}"
 | 
			
		||||
        .value=${entity_id}
 | 
			
		||||
        .name=${"entity_id"}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        allow-custom-entity
 | 
			
		||||
      ></ha-entity-picker>
 | 
			
		||||
      <ha-entity-attribute-picker
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        .entityId=${entity_id}
 | 
			
		||||
        .value=${attribute}
 | 
			
		||||
        .name=${"attribute"}
 | 
			
		||||
        .label=${this.hass.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.triggers.type.state.attribute"
 | 
			
		||||
        )}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
        allow-custom-value
 | 
			
		||||
      ></ha-entity-attribute-picker>
 | 
			
		||||
      <paper-input
 | 
			
		||||
        .label=${this.hass.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.conditions.type.numeric_state.above"
 | 
			
		||||
@@ -60,13 +77,6 @@ export default class HaNumericStateCondition extends LitElement {
 | 
			
		||||
  private _valueChanged(ev: CustomEvent): void {
 | 
			
		||||
    handleChangeEvent(this, ev);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _entityPicked(ev) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    fireEvent(this, "value-changed", {
 | 
			
		||||
      value: { ...this.condition, entity_id: ev.detail.value },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,8 @@
 | 
			
		||||
import "@polymer/paper-input/paper-input";
 | 
			
		||||
import { customElement, html, LitElement, property } from "lit-element";
 | 
			
		||||
import { fireEvent } from "../../../../../common/dom/fire_event";
 | 
			
		||||
import "../../../../../components/entity/ha-entity-attribute-picker";
 | 
			
		||||
import "../../../../../components/entity/ha-entity-picker";
 | 
			
		||||
import { StateCondition } from "../../../../../data/automation";
 | 
			
		||||
import { PolymerChangedEvent } from "../../../../../polymer-types";
 | 
			
		||||
import { HomeAssistant } from "../../../../../types";
 | 
			
		||||
import {
 | 
			
		||||
  ConditionElement,
 | 
			
		||||
@@ -21,15 +20,27 @@ export class HaStateCondition extends LitElement implements ConditionElement {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    const { entity_id, state } = this.condition;
 | 
			
		||||
    const { entity_id, attribute, state } = this.condition;
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <ha-entity-picker
 | 
			
		||||
        .value=${entity_id}
 | 
			
		||||
        @value-changed=${this._entityPicked}
 | 
			
		||||
        .name=${"entity_id"}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        allow-custom-entity
 | 
			
		||||
      ></ha-entity-picker>
 | 
			
		||||
      <ha-entity-attribute-picker
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        .entityId=${entity_id}
 | 
			
		||||
        .value=${attribute}
 | 
			
		||||
        .name=${"attribute"}
 | 
			
		||||
        .label=${this.hass.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.triggers.type.state.attribute"
 | 
			
		||||
        )}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
        allow-custom-value
 | 
			
		||||
      ></ha-entity-attribute-picker>
 | 
			
		||||
      <paper-input
 | 
			
		||||
        .label=${this.hass.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.conditions.type.state.state"
 | 
			
		||||
@@ -44,13 +55,6 @@ export class HaStateCondition extends LitElement implements ConditionElement {
 | 
			
		||||
  private _valueChanged(ev: CustomEvent): void {
 | 
			
		||||
    handleChangeEvent(this, ev);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _entityPicked(ev: PolymerChangedEvent<string>) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    fireEvent(this, "value-changed", {
 | 
			
		||||
      value: { ...this.condition, entity_id: ev.detail.value },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,14 @@
 | 
			
		||||
import { Radio } from "@material/mwc-radio";
 | 
			
		||||
import "@polymer/paper-input/paper-input";
 | 
			
		||||
import { customElement, html, LitElement, property } from "lit-element";
 | 
			
		||||
import {
 | 
			
		||||
  customElement,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import "../../../../../components/ha-formfield";
 | 
			
		||||
import "../../../../../components/ha-radio";
 | 
			
		||||
import { TimeCondition } from "../../../../../data/automation";
 | 
			
		||||
import { HomeAssistant } from "../../../../../types";
 | 
			
		||||
import {
 | 
			
		||||
@@ -7,38 +16,130 @@ import {
 | 
			
		||||
  handleChangeEvent,
 | 
			
		||||
} from "../ha-automation-condition-row";
 | 
			
		||||
 | 
			
		||||
const includeDomains = ["input_datetime"];
 | 
			
		||||
 | 
			
		||||
@customElement("ha-automation-condition-time")
 | 
			
		||||
export class HaTimeCondition extends LitElement implements ConditionElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property() public condition!: TimeCondition;
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _inputModeBefore?: boolean;
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _inputModeAfter?: boolean;
 | 
			
		||||
 | 
			
		||||
  public static get defaultConfig() {
 | 
			
		||||
    return {};
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    const { after, before } = this.condition;
 | 
			
		||||
 | 
			
		||||
    const inputModeBefore =
 | 
			
		||||
      this._inputModeBefore ?? before?.startsWith("input_datetime.");
 | 
			
		||||
    const inputModeAfter =
 | 
			
		||||
      this._inputModeAfter ?? after?.startsWith("input_datetime.");
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <paper-input
 | 
			
		||||
        .label=${this.hass.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.conditions.type.time.after"
 | 
			
		||||
      <ha-formfield
 | 
			
		||||
        .label=${this.hass!.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.conditions.type.time.type_value"
 | 
			
		||||
        )}
 | 
			
		||||
        name="after"
 | 
			
		||||
        .value=${after}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
      ></paper-input>
 | 
			
		||||
      <paper-input
 | 
			
		||||
        .label=${this.hass.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.conditions.type.time.before"
 | 
			
		||||
      >
 | 
			
		||||
        <ha-radio
 | 
			
		||||
          @change=${this._handleModeChanged}
 | 
			
		||||
          name="mode_after"
 | 
			
		||||
          value="value"
 | 
			
		||||
          ?checked=${!inputModeAfter}
 | 
			
		||||
        ></ha-radio>
 | 
			
		||||
      </ha-formfield>
 | 
			
		||||
      <ha-formfield
 | 
			
		||||
        .label=${this.hass!.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.conditions.type.time.type_input"
 | 
			
		||||
        )}
 | 
			
		||||
        name="before"
 | 
			
		||||
        .value=${before}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
      ></paper-input>
 | 
			
		||||
      >
 | 
			
		||||
        <ha-radio
 | 
			
		||||
          @change=${this._handleModeChanged}
 | 
			
		||||
          name="mode_after"
 | 
			
		||||
          value="input"
 | 
			
		||||
          ?checked=${inputModeAfter}
 | 
			
		||||
        ></ha-radio>
 | 
			
		||||
      </ha-formfield>
 | 
			
		||||
      ${inputModeAfter
 | 
			
		||||
        ? html`<ha-entity-picker
 | 
			
		||||
            .label=${this.hass.localize(
 | 
			
		||||
              "ui.panel.config.automation.editor.conditions.type.time.after"
 | 
			
		||||
            )}
 | 
			
		||||
            .includeDomains=${includeDomains}
 | 
			
		||||
            .name=${"after"}
 | 
			
		||||
            .value=${after?.startsWith("input_datetime.") ? after : ""}
 | 
			
		||||
            @value-changed=${this._valueChanged}
 | 
			
		||||
            .hass=${this.hass}
 | 
			
		||||
          ></ha-entity-picker>`
 | 
			
		||||
        : html`<paper-input
 | 
			
		||||
            .label=${this.hass.localize(
 | 
			
		||||
              "ui.panel.config.automation.editor.conditions.type.time.after"
 | 
			
		||||
            )}
 | 
			
		||||
            name="after"
 | 
			
		||||
            .value=${after?.startsWith("input_datetime.") ? "" : after}
 | 
			
		||||
            @value-changed=${this._valueChanged}
 | 
			
		||||
          ></paper-input>`}
 | 
			
		||||
 | 
			
		||||
      <ha-formfield
 | 
			
		||||
        .label=${this.hass!.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.conditions.type.time.type_value"
 | 
			
		||||
        )}
 | 
			
		||||
      >
 | 
			
		||||
        <ha-radio
 | 
			
		||||
          @change=${this._handleModeChanged}
 | 
			
		||||
          name="mode_before"
 | 
			
		||||
          value="value"
 | 
			
		||||
          ?checked=${!inputModeBefore}
 | 
			
		||||
        ></ha-radio>
 | 
			
		||||
      </ha-formfield>
 | 
			
		||||
      <ha-formfield
 | 
			
		||||
        .label=${this.hass!.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.conditions.type.time.type_input"
 | 
			
		||||
        )}
 | 
			
		||||
      >
 | 
			
		||||
        <ha-radio
 | 
			
		||||
          @change=${this._handleModeChanged}
 | 
			
		||||
          name="mode_before"
 | 
			
		||||
          value="input"
 | 
			
		||||
          ?checked=${inputModeBefore}
 | 
			
		||||
        ></ha-radio>
 | 
			
		||||
      </ha-formfield>
 | 
			
		||||
      ${inputModeBefore
 | 
			
		||||
        ? html`<ha-entity-picker
 | 
			
		||||
            .label=${this.hass.localize(
 | 
			
		||||
              "ui.panel.config.automation.editor.conditions.type.time.before"
 | 
			
		||||
            )}
 | 
			
		||||
            .includeDomains=${includeDomains}
 | 
			
		||||
            .name=${"before"}
 | 
			
		||||
            .value=${before?.startsWith("input_datetime.") ? before : ""}
 | 
			
		||||
            @value-changed=${this._valueChanged}
 | 
			
		||||
            .hass=${this.hass}
 | 
			
		||||
          ></ha-entity-picker>`
 | 
			
		||||
        : html`<paper-input
 | 
			
		||||
            .label=${this.hass.localize(
 | 
			
		||||
              "ui.panel.config.automation.editor.conditions.type.time.before"
 | 
			
		||||
            )}
 | 
			
		||||
            name="before"
 | 
			
		||||
            .value=${before?.startsWith("input_datetime.") ? "" : before}
 | 
			
		||||
            @value-changed=${this._valueChanged}
 | 
			
		||||
          ></paper-input>`}
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _handleModeChanged(ev: Event) {
 | 
			
		||||
    const target = ev.target as Radio;
 | 
			
		||||
    if (target.getAttribute("name") === "mode_after") {
 | 
			
		||||
      this._inputModeAfter = target.value === "input";
 | 
			
		||||
    } else {
 | 
			
		||||
      this._inputModeBefore = target.value === "input";
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _valueChanged(ev: CustomEvent): void {
 | 
			
		||||
    handleChangeEvent(this, ev);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,28 +1,32 @@
 | 
			
		||||
import "@material/mwc-fab";
 | 
			
		||||
import { mdiContentDuplicate, mdiContentSave, mdiDelete } from "@mdi/js";
 | 
			
		||||
import "@polymer/app-layout/app-header/app-header";
 | 
			
		||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
 | 
			
		||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
 | 
			
		||||
import "@polymer/paper-input/paper-textarea";
 | 
			
		||||
import "../../../components/ha-icon-button";
 | 
			
		||||
import { PaperListboxElement } from "@polymer/paper-listbox";
 | 
			
		||||
import {
 | 
			
		||||
  css,
 | 
			
		||||
  CSSResult,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  PropertyValues,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { classMap } from "lit-html/directives/class-map";
 | 
			
		||||
import { navigate } from "../../../common/navigate";
 | 
			
		||||
import "../../../components/ha-card";
 | 
			
		||||
import "../../../components/ha-icon-button";
 | 
			
		||||
import "../../../components/ha-svg-icon";
 | 
			
		||||
import "@material/mwc-fab";
 | 
			
		||||
import {
 | 
			
		||||
  AutomationConfig,
 | 
			
		||||
  AutomationEntity,
 | 
			
		||||
  Condition,
 | 
			
		||||
  deleteAutomation,
 | 
			
		||||
  getAutomationEditorInitData,
 | 
			
		||||
  showAutomationEditor,
 | 
			
		||||
  Trigger,
 | 
			
		||||
  triggerAutomation,
 | 
			
		||||
} from "../../../data/automation";
 | 
			
		||||
@@ -42,9 +46,6 @@ import { HaDeviceAction } from "./action/types/ha-automation-action-device_id";
 | 
			
		||||
import "./condition/ha-automation-condition";
 | 
			
		||||
import "./trigger/ha-automation-trigger";
 | 
			
		||||
import { HaDeviceTrigger } from "./trigger/types/ha-automation-trigger-device";
 | 
			
		||||
import { mdiContentSave } from "@mdi/js";
 | 
			
		||||
import { PaperListboxElement } from "@polymer/paper-listbox";
 | 
			
		||||
import { classMap } from "lit-html/directives/class-map";
 | 
			
		||||
 | 
			
		||||
const MODES = ["single", "restart", "queued", "parallel"];
 | 
			
		||||
const MODES_MAX = ["queued", "parallel"];
 | 
			
		||||
@@ -53,6 +54,7 @@ declare global {
 | 
			
		||||
  // for fire event
 | 
			
		||||
  interface HASSDomEvents {
 | 
			
		||||
    "ui-mode-not-available": Error;
 | 
			
		||||
    duplicate: undefined;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -92,14 +94,25 @@ export class HaAutomationEditor extends LitElement {
 | 
			
		||||
        ${!this.automationId
 | 
			
		||||
          ? ""
 | 
			
		||||
          : html`
 | 
			
		||||
              <ha-icon-button
 | 
			
		||||
              <mwc-icon-button
 | 
			
		||||
                slot="toolbar-icon"
 | 
			
		||||
                title="${this.hass.localize(
 | 
			
		||||
                  "ui.panel.config.automation.picker.duplicate_automation"
 | 
			
		||||
                )}"
 | 
			
		||||
                @click=${this._duplicate}
 | 
			
		||||
              >
 | 
			
		||||
                <ha-svg-icon .path=${mdiContentDuplicate}></ha-svg-icon>
 | 
			
		||||
              </mwc-icon-button>
 | 
			
		||||
              <mwc-icon-button
 | 
			
		||||
                class="warning"
 | 
			
		||||
                slot="toolbar-icon"
 | 
			
		||||
                title="${this.hass.localize(
 | 
			
		||||
                  "ui.panel.config.automation.picker.delete_automation"
 | 
			
		||||
                )}"
 | 
			
		||||
                icon="hass:delete"
 | 
			
		||||
                @click=${this._deleteConfirm}
 | 
			
		||||
              ></ha-icon-button>
 | 
			
		||||
              >
 | 
			
		||||
                <ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
 | 
			
		||||
              </mwc-icon-button>
 | 
			
		||||
            `}
 | 
			
		||||
        ${this._config
 | 
			
		||||
          ? html`
 | 
			
		||||
@@ -473,6 +486,31 @@ export class HaAutomationEditor extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _duplicate() {
 | 
			
		||||
    if (this._dirty) {
 | 
			
		||||
      if (
 | 
			
		||||
        !(await showConfirmationDialog(this, {
 | 
			
		||||
          text: this.hass!.localize(
 | 
			
		||||
            "ui.panel.config.automation.editor.unsaved_confirm"
 | 
			
		||||
          ),
 | 
			
		||||
          confirmText: this.hass!.localize("ui.common.yes"),
 | 
			
		||||
          dismissText: this.hass!.localize("ui.common.no"),
 | 
			
		||||
        }))
 | 
			
		||||
      ) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      // Wait for dialog to complate closing
 | 
			
		||||
      await new Promise((resolve) => setTimeout(resolve, 0));
 | 
			
		||||
    }
 | 
			
		||||
    showAutomationEditor(this, {
 | 
			
		||||
      ...this._config,
 | 
			
		||||
      id: undefined,
 | 
			
		||||
      alias: `${this._config?.alias} (${this.hass.localize(
 | 
			
		||||
        "ui.panel.config.automation.picker.duplicate"
 | 
			
		||||
      )})`,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _deleteConfirm() {
 | 
			
		||||
    showConfirmationDialog(this, {
 | 
			
		||||
      text: this.hass.localize(
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ import {
 | 
			
		||||
  showAutomationEditor,
 | 
			
		||||
  triggerAutomation,
 | 
			
		||||
} from "../../../data/automation";
 | 
			
		||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
 | 
			
		||||
import "../../../layouts/hass-tabs-subpage-data-table";
 | 
			
		||||
import { haStyle } from "../../../resources/styles";
 | 
			
		||||
import { HomeAssistant, Route } from "../../../types";
 | 
			
		||||
@@ -35,9 +36,9 @@ import { showThingtalkDialog } from "./show-dialog-thingtalk";
 | 
			
		||||
class HaAutomationPicker extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property() public isWide!: boolean;
 | 
			
		||||
  @property({ type: Boolean }) public isWide!: boolean;
 | 
			
		||||
 | 
			
		||||
  @property() public narrow!: boolean;
 | 
			
		||||
  @property({ type: Boolean }) public narrow!: boolean;
 | 
			
		||||
 | 
			
		||||
  @property() public route!: Route;
 | 
			
		||||
 | 
			
		||||
@@ -58,7 +59,7 @@ class HaAutomationPicker extends LitElement {
 | 
			
		||||
        toggle: {
 | 
			
		||||
          title: "",
 | 
			
		||||
          type: "icon",
 | 
			
		||||
          template: (_toggle, automation) =>
 | 
			
		||||
          template: (_toggle, automation: any) =>
 | 
			
		||||
            html`
 | 
			
		||||
              <ha-entity-toggle
 | 
			
		||||
                .hass=${this.hass}
 | 
			
		||||
@@ -91,10 +92,11 @@ class HaAutomationPicker extends LitElement {
 | 
			
		||||
      if (!narrow) {
 | 
			
		||||
        columns.execute = {
 | 
			
		||||
          title: "",
 | 
			
		||||
          template: (_info, automation) => html`
 | 
			
		||||
          template: (_info, automation: any) => html`
 | 
			
		||||
            <mwc-button
 | 
			
		||||
              .automation=${automation}
 | 
			
		||||
              @click=${(ev) => this._execute(ev)}
 | 
			
		||||
              .disabled=${UNAVAILABLE_STATES.includes(automation.state)}
 | 
			
		||||
            >
 | 
			
		||||
              ${this.hass.localize("ui.card.automation.trigger")}
 | 
			
		||||
            </mwc-button>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +1,27 @@
 | 
			
		||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
 | 
			
		||||
import "@material/mwc-list/mwc-list-item";
 | 
			
		||||
import { mdiDotsVertical } from "@mdi/js";
 | 
			
		||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
 | 
			
		||||
import "../../../../components/ha-icon-button";
 | 
			
		||||
import "@polymer/paper-item/paper-item";
 | 
			
		||||
import "@polymer/paper-listbox/paper-listbox";
 | 
			
		||||
import "@material/mwc-list/mwc-list-item";
 | 
			
		||||
import "../../../../components/ha-button-menu";
 | 
			
		||||
import { mdiDotsVertical } from "@mdi/js";
 | 
			
		||||
import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
 | 
			
		||||
import {
 | 
			
		||||
  css,
 | 
			
		||||
  CSSResult,
 | 
			
		||||
  customElement,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
 | 
			
		||||
import { fireEvent } from "../../../../common/dom/fire_event";
 | 
			
		||||
import "../../../../components/ha-button-menu";
 | 
			
		||||
import "../../../../components/ha-card";
 | 
			
		||||
import "../../../../components/ha-icon-button";
 | 
			
		||||
import type { Trigger } from "../../../../data/automation";
 | 
			
		||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
 | 
			
		||||
import { haStyle } from "../../../../resources/styles";
 | 
			
		||||
import type { HomeAssistant } from "../../../../types";
 | 
			
		||||
import "./types/ha-automation-trigger-device";
 | 
			
		||||
import "./types/ha-automation-trigger-event";
 | 
			
		||||
@@ -29,14 +31,12 @@ import "./types/ha-automation-trigger-mqtt";
 | 
			
		||||
import "./types/ha-automation-trigger-numeric_state";
 | 
			
		||||
import "./types/ha-automation-trigger-state";
 | 
			
		||||
import "./types/ha-automation-trigger-sun";
 | 
			
		||||
import "./types/ha-automation-trigger-tag";
 | 
			
		||||
import "./types/ha-automation-trigger-template";
 | 
			
		||||
import "./types/ha-automation-trigger-time";
 | 
			
		||||
import "./types/ha-automation-trigger-time_pattern";
 | 
			
		||||
import "./types/ha-automation-trigger-webhook";
 | 
			
		||||
import "./types/ha-automation-trigger-zone";
 | 
			
		||||
import "./types/ha-automation-trigger-tag";
 | 
			
		||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
 | 
			
		||||
import { haStyle } from "../../../../resources/styles";
 | 
			
		||||
 | 
			
		||||
const OPTIONS = [
 | 
			
		||||
  "device",
 | 
			
		||||
@@ -113,12 +113,12 @@ export default class HaAutomationTriggerRow extends LitElement {
 | 
			
		||||
                      "ui.panel.config.automation.editor.edit_yaml"
 | 
			
		||||
                    )}
 | 
			
		||||
              </mwc-list-item>
 | 
			
		||||
              <mwc-list-item disabled>
 | 
			
		||||
              <mwc-list-item>
 | 
			
		||||
                ${this.hass.localize(
 | 
			
		||||
                  "ui.panel.config.automation.editor.actions.duplicate"
 | 
			
		||||
                )}
 | 
			
		||||
              </mwc-list-item>
 | 
			
		||||
              <mwc-list-item>
 | 
			
		||||
              <mwc-list-item class="warning">
 | 
			
		||||
                ${this.hass.localize(
 | 
			
		||||
                  "ui.panel.config.automation.editor.actions.delete"
 | 
			
		||||
                )}
 | 
			
		||||
@@ -183,6 +183,7 @@ export default class HaAutomationTriggerRow extends LitElement {
 | 
			
		||||
        this._switchYamlMode();
 | 
			
		||||
        break;
 | 
			
		||||
      case 1:
 | 
			
		||||
        fireEvent(this, "duplicate");
 | 
			
		||||
        break;
 | 
			
		||||
      case 2:
 | 
			
		||||
        this._onDelete();
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@ export default class HaAutomationTrigger extends LitElement {
 | 
			
		||||
          <ha-automation-trigger-row
 | 
			
		||||
            .index=${idx}
 | 
			
		||||
            .trigger=${trg}
 | 
			
		||||
            @duplicate=${this._duplicateTrigger}
 | 
			
		||||
            @value-changed=${this._triggerChanged}
 | 
			
		||||
            .hass=${this.hass}
 | 
			
		||||
          ></ha-automation-trigger-row>
 | 
			
		||||
@@ -68,6 +69,14 @@ export default class HaAutomationTrigger extends LitElement {
 | 
			
		||||
    fireEvent(this, "value-changed", { value: triggers });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _duplicateTrigger(ev: CustomEvent) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    const index = (ev.target as any).index;
 | 
			
		||||
    fireEvent(this, "value-changed", {
 | 
			
		||||
      value: this.triggers.concat(this.triggers[index]),
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get styles(): CSSResult {
 | 
			
		||||
    return css`
 | 
			
		||||
      ha-automation-trigger-row,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import "@polymer/paper-input/paper-input";
 | 
			
		||||
import "@polymer/paper-input/paper-textarea";
 | 
			
		||||
import { customElement, html, LitElement, property } from "lit-element";
 | 
			
		||||
import { fireEvent } from "../../../../../common/dom/fire_event";
 | 
			
		||||
import "../../../../../components/entity/ha-entity-picker";
 | 
			
		||||
import { ForDict, NumericStateTrigger } from "../../../../../data/automation";
 | 
			
		||||
import { HomeAssistant } from "../../../../../types";
 | 
			
		||||
@@ -19,8 +18,8 @@ export default class HaNumericStateTrigger extends LitElement {
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    const { value_template, entity_id, below, above } = this.trigger;
 | 
			
		||||
  public render() {
 | 
			
		||||
    const { value_template, entity_id, attribute, below, above } = this.trigger;
 | 
			
		||||
    let trgFor = this.trigger.for;
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
@@ -41,10 +40,22 @@ export default class HaNumericStateTrigger extends LitElement {
 | 
			
		||||
    return html`
 | 
			
		||||
      <ha-entity-picker
 | 
			
		||||
        .value="${entity_id}"
 | 
			
		||||
        @value-changed="${this._entityPicked}"
 | 
			
		||||
        @value-changed="${this._valueChanged}"
 | 
			
		||||
        .name=${"entity_id"}
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        allow-custom-entity
 | 
			
		||||
      ></ha-entity-picker>
 | 
			
		||||
      <ha-entity-attribute-picker
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        .entityId=${entity_id}
 | 
			
		||||
        .value=${attribute}
 | 
			
		||||
        .name=${"attribute"}
 | 
			
		||||
        .label=${this.hass.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.triggers.type.state.attribute"
 | 
			
		||||
        )}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
        allow-custom-value
 | 
			
		||||
      ></ha-entity-attribute-picker>
 | 
			
		||||
      <paper-input
 | 
			
		||||
        .label=${this.hass.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.triggers.type.numeric_state.above"
 | 
			
		||||
@@ -84,13 +95,6 @@ export default class HaNumericStateTrigger extends LitElement {
 | 
			
		||||
  private _valueChanged(ev: CustomEvent): void {
 | 
			
		||||
    handleChangeEvent(this, ev);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _entityPicked(ev) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    fireEvent(this, "value-changed", {
 | 
			
		||||
      value: { ...this.trigger, entity_id: ev.detail.value },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,8 @@
 | 
			
		||||
import "@polymer/paper-input/paper-input";
 | 
			
		||||
import { customElement, html, LitElement, property } from "lit-element";
 | 
			
		||||
import { fireEvent } from "../../../../../common/dom/fire_event";
 | 
			
		||||
import "../../../../../components/entity/ha-entity-attribute-picker";
 | 
			
		||||
import "../../../../../components/entity/ha-entity-picker";
 | 
			
		||||
import { ForDict, StateTrigger } from "../../../../../data/automation";
 | 
			
		||||
import { PolymerChangedEvent } from "../../../../../polymer-types";
 | 
			
		||||
import { HomeAssistant } from "../../../../../types";
 | 
			
		||||
import {
 | 
			
		||||
  handleChangeEvent,
 | 
			
		||||
@@ -21,7 +20,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    const { entity_id, to, from } = this.trigger;
 | 
			
		||||
    const { entity_id, attribute, to, from } = this.trigger;
 | 
			
		||||
    let trgFor = this.trigger.for;
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
@@ -43,10 +42,22 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
 | 
			
		||||
    return html`
 | 
			
		||||
      <ha-entity-picker
 | 
			
		||||
        .value=${entity_id}
 | 
			
		||||
        @value-changed=${this._entityPicked}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
        .name=${"entity_id"}
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        allow-custom-entity
 | 
			
		||||
      ></ha-entity-picker>
 | 
			
		||||
      <ha-entity-attribute-picker
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        .entityId=${entity_id}
 | 
			
		||||
        .value=${attribute}
 | 
			
		||||
        .name=${"attribute"}
 | 
			
		||||
        .label=${this.hass.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.triggers.type.state.attribute"
 | 
			
		||||
        )}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
        allow-custom-value
 | 
			
		||||
      ></ha-entity-attribute-picker>
 | 
			
		||||
      <paper-input
 | 
			
		||||
        .label=${this.hass.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.triggers.type.state.from"
 | 
			
		||||
@@ -77,13 +88,6 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
 | 
			
		||||
  private _valueChanged(ev: CustomEvent): void {
 | 
			
		||||
    handleChangeEvent(this, ev);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _entityPicked(ev: PolymerChangedEvent<string>) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    fireEvent(this, "value-changed", {
 | 
			
		||||
      value: { ...this.trigger, entity_id: ev.detail.value },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,14 @@
 | 
			
		||||
import "@polymer/paper-input/paper-input";
 | 
			
		||||
import { customElement, html, LitElement, property } from "lit-element";
 | 
			
		||||
import {
 | 
			
		||||
  customElement,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import "../../../../../components/entity/ha-entity-picker";
 | 
			
		||||
import "../../../../../components/ha-formfield";
 | 
			
		||||
import "../../../../../components/ha-radio";
 | 
			
		||||
import { TimeTrigger } from "../../../../../data/automation";
 | 
			
		||||
import { HomeAssistant } from "../../../../../types";
 | 
			
		||||
import {
 | 
			
		||||
@@ -7,31 +16,81 @@ import {
 | 
			
		||||
  TriggerElement,
 | 
			
		||||
} from "../ha-automation-trigger-row";
 | 
			
		||||
 | 
			
		||||
const includeDomains = ["input_datetime"];
 | 
			
		||||
 | 
			
		||||
@customElement("ha-automation-trigger-time")
 | 
			
		||||
export class HaTimeTrigger extends LitElement implements TriggerElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property() public trigger!: TimeTrigger;
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _inputMode?: boolean;
 | 
			
		||||
 | 
			
		||||
  public static get defaultConfig() {
 | 
			
		||||
    return { at: "" };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    const { at } = this.trigger;
 | 
			
		||||
    const inputMode = this._inputMode ?? at?.startsWith("input_datetime.");
 | 
			
		||||
    return html`
 | 
			
		||||
      <paper-input
 | 
			
		||||
        .label=${this.hass.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.triggers.type.time.at"
 | 
			
		||||
      <ha-formfield
 | 
			
		||||
        .label=${this.hass!.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.triggers.type.time.type_value"
 | 
			
		||||
        )}
 | 
			
		||||
        name="at"
 | 
			
		||||
        .value=${at}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
      ></paper-input>
 | 
			
		||||
      >
 | 
			
		||||
        <ha-radio
 | 
			
		||||
          @change=${this._handleModeChanged}
 | 
			
		||||
          name="mode"
 | 
			
		||||
          value="value"
 | 
			
		||||
          ?checked=${!inputMode}
 | 
			
		||||
        ></ha-radio>
 | 
			
		||||
      </ha-formfield>
 | 
			
		||||
      <ha-formfield
 | 
			
		||||
        .label=${this.hass!.localize(
 | 
			
		||||
          "ui.panel.config.automation.editor.triggers.type.time.type_input"
 | 
			
		||||
        )}
 | 
			
		||||
      >
 | 
			
		||||
        <ha-radio
 | 
			
		||||
          @change=${this._handleModeChanged}
 | 
			
		||||
          name="mode"
 | 
			
		||||
          value="input"
 | 
			
		||||
          ?checked=${inputMode}
 | 
			
		||||
        ></ha-radio>
 | 
			
		||||
      </ha-formfield>
 | 
			
		||||
      ${inputMode
 | 
			
		||||
        ? html`<ha-entity-picker
 | 
			
		||||
            .label=${this.hass.localize(
 | 
			
		||||
              "ui.panel.config.automation.editor.triggers.type.time.at"
 | 
			
		||||
            )}
 | 
			
		||||
            .includeDomains=${includeDomains}
 | 
			
		||||
            .name=${"at"}
 | 
			
		||||
            .value=${at?.startsWith("input_datetime.") ? at : ""}
 | 
			
		||||
            @value-changed=${this._valueChanged}
 | 
			
		||||
            .hass=${this.hass}
 | 
			
		||||
          ></ha-entity-picker>`
 | 
			
		||||
        : html`<paper-input
 | 
			
		||||
            .label=${this.hass.localize(
 | 
			
		||||
              "ui.panel.config.automation.editor.triggers.type.time.at"
 | 
			
		||||
            )}
 | 
			
		||||
            name="at"
 | 
			
		||||
            .value=${at?.startsWith("input_datetime.") ? "" : at}
 | 
			
		||||
            @value-changed=${this._valueChanged}
 | 
			
		||||
          ></paper-input>`}
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _handleModeChanged(ev: Event) {
 | 
			
		||||
    this._inputMode = (ev.target as any).value === "input";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _valueChanged(ev: CustomEvent): void {
 | 
			
		||||
    handleChangeEvent(this, ev);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-automation-trigger-time": HaTimeTrigger;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,9 @@ class CloudAlexa extends LitElement {
 | 
			
		||||
 | 
			
		||||
    this._entities.forEach((entity) => {
 | 
			
		||||
      const stateObj = this.hass.states[entity.entity_id];
 | 
			
		||||
      const config = this._entityConfigs[entity.entity_id] || {};
 | 
			
		||||
      const config = this._entityConfigs[entity.entity_id] || {
 | 
			
		||||
        should_expose: null,
 | 
			
		||||
      };
 | 
			
		||||
      const isExposed = emptyFilter
 | 
			
		||||
        ? this._configIsExposed(entity.entity_id, config)
 | 
			
		||||
        : filterFunc(entity.entity_id);
 | 
			
		||||
@@ -319,9 +321,7 @@ class CloudAlexa extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _configIsExposed(entityId: string, config: AlexaEntityConfig) {
 | 
			
		||||
    return config.should_expose === null
 | 
			
		||||
      ? this._configIsDomainExposed(entityId)
 | 
			
		||||
      : config.should_expose;
 | 
			
		||||
    return config.should_expose ?? this._configIsDomainExposed(entityId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _exposeChanged(ev: CustomEvent<ActionDetail>) {
 | 
			
		||||
 
 | 
			
		||||
@@ -109,7 +109,9 @@ class CloudGoogleAssistant extends LitElement {
 | 
			
		||||
 | 
			
		||||
    this._entities.forEach((entity) => {
 | 
			
		||||
      const stateObj = this.hass.states[entity.entity_id];
 | 
			
		||||
      const config = this._entityConfigs[entity.entity_id] || {};
 | 
			
		||||
      const config = this._entityConfigs[entity.entity_id] || {
 | 
			
		||||
        should_expose: null,
 | 
			
		||||
      };
 | 
			
		||||
      const isExposed = emptyFilter
 | 
			
		||||
        ? this._configIsExposed(entity.entity_id, config)
 | 
			
		||||
        : filterFunc(entity.entity_id);
 | 
			
		||||
@@ -324,9 +326,7 @@ class CloudGoogleAssistant extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _configIsExposed(entityId: string, config: GoogleEntityConfig) {
 | 
			
		||||
    return config.should_expose === null
 | 
			
		||||
      ? this._configIsDomainExposed(entityId)
 | 
			
		||||
      : config.should_expose;
 | 
			
		||||
    return config.should_expose ?? this._configIsDomainExposed(entityId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _fetchData() {
 | 
			
		||||
 
 | 
			
		||||
@@ -58,6 +58,8 @@ import {
 | 
			
		||||
  loadEntityEditorDialog,
 | 
			
		||||
  showEntityEditorDialog,
 | 
			
		||||
} from "./show-dialog-entity-editor";
 | 
			
		||||
import { haStyle } from "../../../resources/styles";
 | 
			
		||||
import { UNAVAILABLE } from "../../../data/entity";
 | 
			
		||||
 | 
			
		||||
export interface StateEntity extends EntityRegistryEntry {
 | 
			
		||||
  readonly?: boolean;
 | 
			
		||||
@@ -280,7 +282,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
 | 
			
		||||
 | 
			
		||||
      for (const entry of entities) {
 | 
			
		||||
        const entity = this.hass.states[entry.entity_id];
 | 
			
		||||
        const unavailable = entity?.state === "unavailable";
 | 
			
		||||
        const unavailable = entity?.state === UNAVAILABLE;
 | 
			
		||||
        const restored = entity?.attributes.restored;
 | 
			
		||||
 | 
			
		||||
        if (!showUnavailable && unavailable) {
 | 
			
		||||
@@ -378,7 +380,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
 | 
			
		||||
                      "ui.panel.config.entities.picker.disable_selected.button"
 | 
			
		||||
                    )}</mwc-button
 | 
			
		||||
                  >
 | 
			
		||||
                  <mwc-button @click=${this._removeSelected}
 | 
			
		||||
                  <mwc-button @click=${this._removeSelected} class="warning"
 | 
			
		||||
                    >${this.hass.localize(
 | 
			
		||||
                      "ui.panel.config.entities.picker.remove_selected.button"
 | 
			
		||||
                    )}</mwc-button
 | 
			
		||||
@@ -406,6 +408,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
 | 
			
		||||
                    )}
 | 
			
		||||
                  </paper-tooltip>
 | 
			
		||||
                  <ha-icon-button
 | 
			
		||||
                    class="warning"
 | 
			
		||||
                    id="remove-btn"
 | 
			
		||||
                    icon="hass:delete"
 | 
			
		||||
                    @click=${this._removeSelected}
 | 
			
		||||
@@ -721,111 +724,114 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
 | 
			
		||||
    navigate(this, window.location.pathname, true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get styles(): CSSResult {
 | 
			
		||||
    return css`
 | 
			
		||||
      hass-loading-screen {
 | 
			
		||||
        --app-header-background-color: var(--sidebar-background-color);
 | 
			
		||||
        --app-header-text-color: var(--sidebar-text-color);
 | 
			
		||||
      }
 | 
			
		||||
      a {
 | 
			
		||||
        color: var(--primary-color);
 | 
			
		||||
      }
 | 
			
		||||
      h2 {
 | 
			
		||||
        margin-top: 0;
 | 
			
		||||
        font-family: var(--paper-font-headline_-_font-family);
 | 
			
		||||
        -webkit-font-smoothing: var(
 | 
			
		||||
          --paper-font-headline_-_-webkit-font-smoothing
 | 
			
		||||
        );
 | 
			
		||||
        font-size: var(--paper-font-headline_-_font-size);
 | 
			
		||||
        font-weight: var(--paper-font-headline_-_font-weight);
 | 
			
		||||
        letter-spacing: var(--paper-font-headline_-_letter-spacing);
 | 
			
		||||
        line-height: var(--paper-font-headline_-_line-height);
 | 
			
		||||
        opacity: var(--dark-primary-opacity);
 | 
			
		||||
      }
 | 
			
		||||
      p {
 | 
			
		||||
        font-family: var(--paper-font-subhead_-_font-family);
 | 
			
		||||
        -webkit-font-smoothing: var(
 | 
			
		||||
          --paper-font-subhead_-_-webkit-font-smoothing
 | 
			
		||||
        );
 | 
			
		||||
        font-weight: var(--paper-font-subhead_-_font-weight);
 | 
			
		||||
        line-height: var(--paper-font-subhead_-_line-height);
 | 
			
		||||
      }
 | 
			
		||||
      ha-data-table {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        --data-table-border-width: 0;
 | 
			
		||||
      }
 | 
			
		||||
      :host(:not([narrow])) ha-data-table {
 | 
			
		||||
        height: calc(100vh - 65px);
 | 
			
		||||
        display: block;
 | 
			
		||||
      }
 | 
			
		||||
      ha-button-menu {
 | 
			
		||||
        margin-right: 8px;
 | 
			
		||||
      }
 | 
			
		||||
      .table-header {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        justify-content: space-between;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
 | 
			
		||||
      }
 | 
			
		||||
      search-input {
 | 
			
		||||
        margin-left: 16px;
 | 
			
		||||
        flex-grow: 1;
 | 
			
		||||
        position: relative;
 | 
			
		||||
        top: 2px;
 | 
			
		||||
      }
 | 
			
		||||
      .search-toolbar search-input {
 | 
			
		||||
        margin-left: 8px;
 | 
			
		||||
        top: 1px;
 | 
			
		||||
      }
 | 
			
		||||
      .search-toolbar {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        justify-content: space-between;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        color: var(--secondary-text-color);
 | 
			
		||||
      }
 | 
			
		||||
      .search-toolbar ha-button-menu {
 | 
			
		||||
        position: static;
 | 
			
		||||
      }
 | 
			
		||||
      .selected-txt {
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
        padding-left: 16px;
 | 
			
		||||
      }
 | 
			
		||||
      .table-header .selected-txt {
 | 
			
		||||
        margin-top: 20px;
 | 
			
		||||
      }
 | 
			
		||||
      .search-toolbar .selected-txt {
 | 
			
		||||
        font-size: 16px;
 | 
			
		||||
      }
 | 
			
		||||
      .header-btns > mwc-button,
 | 
			
		||||
      .header-btns > ha-icon-button {
 | 
			
		||||
        margin: 8px;
 | 
			
		||||
      }
 | 
			
		||||
      .active-filters {
 | 
			
		||||
        color: var(--primary-text-color);
 | 
			
		||||
        position: relative;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        padding: 2px 2px 2px 8px;
 | 
			
		||||
        margin-left: 4px;
 | 
			
		||||
        font-size: 14px;
 | 
			
		||||
      }
 | 
			
		||||
      .active-filters ha-icon {
 | 
			
		||||
        color: var(--primary-color);
 | 
			
		||||
      }
 | 
			
		||||
      .active-filters mwc-button {
 | 
			
		||||
        margin-left: 8px;
 | 
			
		||||
      }
 | 
			
		||||
      .active-filters::before {
 | 
			
		||||
        background-color: var(--primary-color);
 | 
			
		||||
        opacity: 0.12;
 | 
			
		||||
        border-radius: 4px;
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        top: 0;
 | 
			
		||||
        right: 0;
 | 
			
		||||
        bottom: 0;
 | 
			
		||||
        left: 0;
 | 
			
		||||
        content: "";
 | 
			
		||||
      }
 | 
			
		||||
    `;
 | 
			
		||||
  static get styles(): CSSResult[] {
 | 
			
		||||
    return [
 | 
			
		||||
      haStyle,
 | 
			
		||||
      css`
 | 
			
		||||
        hass-loading-screen {
 | 
			
		||||
          --app-header-background-color: var(--sidebar-background-color);
 | 
			
		||||
          --app-header-text-color: var(--sidebar-text-color);
 | 
			
		||||
        }
 | 
			
		||||
        a {
 | 
			
		||||
          color: var(--primary-color);
 | 
			
		||||
        }
 | 
			
		||||
        h2 {
 | 
			
		||||
          margin-top: 0;
 | 
			
		||||
          font-family: var(--paper-font-headline_-_font-family);
 | 
			
		||||
          -webkit-font-smoothing: var(
 | 
			
		||||
            --paper-font-headline_-_-webkit-font-smoothing
 | 
			
		||||
          );
 | 
			
		||||
          font-size: var(--paper-font-headline_-_font-size);
 | 
			
		||||
          font-weight: var(--paper-font-headline_-_font-weight);
 | 
			
		||||
          letter-spacing: var(--paper-font-headline_-_letter-spacing);
 | 
			
		||||
          line-height: var(--paper-font-headline_-_line-height);
 | 
			
		||||
          opacity: var(--dark-primary-opacity);
 | 
			
		||||
        }
 | 
			
		||||
        p {
 | 
			
		||||
          font-family: var(--paper-font-subhead_-_font-family);
 | 
			
		||||
          -webkit-font-smoothing: var(
 | 
			
		||||
            --paper-font-subhead_-_-webkit-font-smoothing
 | 
			
		||||
          );
 | 
			
		||||
          font-weight: var(--paper-font-subhead_-_font-weight);
 | 
			
		||||
          line-height: var(--paper-font-subhead_-_line-height);
 | 
			
		||||
        }
 | 
			
		||||
        ha-data-table {
 | 
			
		||||
          width: 100%;
 | 
			
		||||
          --data-table-border-width: 0;
 | 
			
		||||
        }
 | 
			
		||||
        :host(:not([narrow])) ha-data-table {
 | 
			
		||||
          height: calc(100vh - 65px);
 | 
			
		||||
          display: block;
 | 
			
		||||
        }
 | 
			
		||||
        ha-button-menu {
 | 
			
		||||
          margin-right: 8px;
 | 
			
		||||
        }
 | 
			
		||||
        .table-header {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          justify-content: space-between;
 | 
			
		||||
          align-items: center;
 | 
			
		||||
          border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
 | 
			
		||||
        }
 | 
			
		||||
        search-input {
 | 
			
		||||
          margin-left: 16px;
 | 
			
		||||
          flex-grow: 1;
 | 
			
		||||
          position: relative;
 | 
			
		||||
          top: 2px;
 | 
			
		||||
        }
 | 
			
		||||
        .search-toolbar search-input {
 | 
			
		||||
          margin-left: 8px;
 | 
			
		||||
          top: 1px;
 | 
			
		||||
        }
 | 
			
		||||
        .search-toolbar {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          justify-content: space-between;
 | 
			
		||||
          align-items: center;
 | 
			
		||||
          color: var(--secondary-text-color);
 | 
			
		||||
        }
 | 
			
		||||
        .search-toolbar ha-button-menu {
 | 
			
		||||
          position: static;
 | 
			
		||||
        }
 | 
			
		||||
        .selected-txt {
 | 
			
		||||
          font-weight: bold;
 | 
			
		||||
          padding-left: 16px;
 | 
			
		||||
        }
 | 
			
		||||
        .table-header .selected-txt {
 | 
			
		||||
          margin-top: 20px;
 | 
			
		||||
        }
 | 
			
		||||
        .search-toolbar .selected-txt {
 | 
			
		||||
          font-size: 16px;
 | 
			
		||||
        }
 | 
			
		||||
        .header-btns > mwc-button,
 | 
			
		||||
        .header-btns > ha-icon-button {
 | 
			
		||||
          margin: 8px;
 | 
			
		||||
        }
 | 
			
		||||
        .active-filters {
 | 
			
		||||
          color: var(--primary-text-color);
 | 
			
		||||
          position: relative;
 | 
			
		||||
          display: flex;
 | 
			
		||||
          align-items: center;
 | 
			
		||||
          padding: 2px 2px 2px 8px;
 | 
			
		||||
          margin-left: 4px;
 | 
			
		||||
          font-size: 14px;
 | 
			
		||||
        }
 | 
			
		||||
        .active-filters ha-icon {
 | 
			
		||||
          color: var(--primary-color);
 | 
			
		||||
        }
 | 
			
		||||
        .active-filters mwc-button {
 | 
			
		||||
          margin-left: 8px;
 | 
			
		||||
        }
 | 
			
		||||
        .active-filters::before {
 | 
			
		||||
          background-color: var(--primary-color);
 | 
			
		||||
          opacity: 0.12;
 | 
			
		||||
          border-radius: 4px;
 | 
			
		||||
          position: absolute;
 | 
			
		||||
          top: 0;
 | 
			
		||||
          right: 0;
 | 
			
		||||
          bottom: 0;
 | 
			
		||||
          left: 0;
 | 
			
		||||
          content: "";
 | 
			
		||||
        }
 | 
			
		||||
      `,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,9 +6,9 @@ import {
 | 
			
		||||
  CSSResult,
 | 
			
		||||
  customElement,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { fireEvent } from "../../../../common/dom/fire_event";
 | 
			
		||||
@@ -48,12 +48,12 @@ class HaInputNumberForm extends LitElement {
 | 
			
		||||
      this._max = item.max ?? 100;
 | 
			
		||||
      this._min = item.min ?? 0;
 | 
			
		||||
      this._mode = item.mode || "slider";
 | 
			
		||||
      this._step = item.step || 1;
 | 
			
		||||
      this._step = item.step ?? 1;
 | 
			
		||||
      this._unit_of_measurement = item.unit_of_measurement;
 | 
			
		||||
    } else {
 | 
			
		||||
      this._item = {
 | 
			
		||||
        min: 0,
 | 
			
		||||
        max: 0,
 | 
			
		||||
        max: 100,
 | 
			
		||||
      };
 | 
			
		||||
      this._name = "";
 | 
			
		||||
      this._icon = "";
 | 
			
		||||
@@ -176,8 +176,10 @@ class HaInputNumberForm extends LitElement {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    const configValue = (ev.target as any).configValue;
 | 
			
		||||
    const value = ev.detail.value;
 | 
			
		||||
    const target = ev.target as any;
 | 
			
		||||
    const configValue = target.configValue;
 | 
			
		||||
    const value =
 | 
			
		||||
      target.type === "number" ? Number(ev.detail.value) : ev.detail.value;
 | 
			
		||||
    if (this[`_${configValue}`] === value) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,12 +10,13 @@ import {
 | 
			
		||||
  CSSResult,
 | 
			
		||||
  customElement,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  PropertyValues,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { classMap } from "lit-html/directives/class-map";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
 | 
			
		||||
import "../../../common/search/search-input";
 | 
			
		||||
@@ -32,6 +33,7 @@ import {
 | 
			
		||||
  getConfigEntries,
 | 
			
		||||
} from "../../../data/config_entries";
 | 
			
		||||
import {
 | 
			
		||||
  ATTENTION_SOURCES,
 | 
			
		||||
  DISCOVERY_SOURCES,
 | 
			
		||||
  getConfigFlowInProgressCollection,
 | 
			
		||||
  ignoreConfigFlow,
 | 
			
		||||
@@ -355,52 +357,67 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
 | 
			
		||||
            : ""}
 | 
			
		||||
          ${configEntriesInProgress.length
 | 
			
		||||
            ? configEntriesInProgress.map(
 | 
			
		||||
                (flow: DataEntryFlowProgressExtended) => html`
 | 
			
		||||
                  <ha-card outlined class="discovered">
 | 
			
		||||
                    <div class="header">
 | 
			
		||||
                      ${this.hass.localize(
 | 
			
		||||
                        "ui.panel.config.integrations.discovered"
 | 
			
		||||
                      )}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="card-content">
 | 
			
		||||
                      <div class="image">
 | 
			
		||||
                        <img
 | 
			
		||||
                          src="https://brands.home-assistant.io/${flow.handler}/logo.png"
 | 
			
		||||
                          referrerpolicy="no-referrer"
 | 
			
		||||
                          @error=${this._onImageError}
 | 
			
		||||
                          @load=${this._onImageLoad}
 | 
			
		||||
                        />
 | 
			
		||||
                (flow: DataEntryFlowProgressExtended) => {
 | 
			
		||||
                  const attention = ATTENTION_SOURCES.includes(
 | 
			
		||||
                    flow.context.source
 | 
			
		||||
                  );
 | 
			
		||||
                  return html`
 | 
			
		||||
                    <ha-card
 | 
			
		||||
                      outlined
 | 
			
		||||
                      class=${classMap({
 | 
			
		||||
                        discovered: !attention,
 | 
			
		||||
                        attention: attention,
 | 
			
		||||
                      })}
 | 
			
		||||
                    >
 | 
			
		||||
                      <div class="header">
 | 
			
		||||
                        ${this.hass.localize(
 | 
			
		||||
                          `ui.panel.config.integrations.${
 | 
			
		||||
                            attention ? "attention" : "discovered"
 | 
			
		||||
                          }`
 | 
			
		||||
                        )}
 | 
			
		||||
                      </div>
 | 
			
		||||
                      <h2>
 | 
			
		||||
                        ${flow.localized_title}
 | 
			
		||||
                      </h2>
 | 
			
		||||
                      <div>
 | 
			
		||||
                        <mwc-button
 | 
			
		||||
                          unelevated
 | 
			
		||||
                          @click=${this._continueFlow}
 | 
			
		||||
                          .flowId=${flow.flow_id}
 | 
			
		||||
                        >
 | 
			
		||||
                          ${this.hass.localize(
 | 
			
		||||
                            "ui.panel.config.integrations.configure"
 | 
			
		||||
                          )}
 | 
			
		||||
                        </mwc-button>
 | 
			
		||||
                        ${DISCOVERY_SOURCES.includes(flow.context.source) &&
 | 
			
		||||
                        flow.context.unique_id
 | 
			
		||||
                          ? html`
 | 
			
		||||
                              <mwc-button
 | 
			
		||||
                                @click=${this._ignoreFlow}
 | 
			
		||||
                                .flow=${flow}
 | 
			
		||||
                              >
 | 
			
		||||
                                ${this.hass.localize(
 | 
			
		||||
                                  "ui.panel.config.integrations.ignore.ignore"
 | 
			
		||||
                                )}
 | 
			
		||||
                              </mwc-button>
 | 
			
		||||
                            `
 | 
			
		||||
                          : ""}
 | 
			
		||||
                      <div class="card-content">
 | 
			
		||||
                        <div class="image">
 | 
			
		||||
                          <img
 | 
			
		||||
                            src="https://brands.home-assistant.io/${flow.handler}/logo.png"
 | 
			
		||||
                            referrerpolicy="no-referrer"
 | 
			
		||||
                            @error=${this._onImageError}
 | 
			
		||||
                            @load=${this._onImageLoad}
 | 
			
		||||
                          />
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <h2>
 | 
			
		||||
                          ${flow.localized_title}
 | 
			
		||||
                        </h2>
 | 
			
		||||
                        <div>
 | 
			
		||||
                          <mwc-button
 | 
			
		||||
                            unelevated
 | 
			
		||||
                            @click=${this._continueFlow}
 | 
			
		||||
                            .flowId=${flow.flow_id}
 | 
			
		||||
                          >
 | 
			
		||||
                            ${this.hass.localize(
 | 
			
		||||
                              `ui.panel.config.integrations.${
 | 
			
		||||
                                attention ? "reconfigure" : "configure"
 | 
			
		||||
                              }`
 | 
			
		||||
                            )}
 | 
			
		||||
                          </mwc-button>
 | 
			
		||||
                          ${DISCOVERY_SOURCES.includes(flow.context.source) &&
 | 
			
		||||
                          flow.context.unique_id
 | 
			
		||||
                            ? html`
 | 
			
		||||
                                <mwc-button
 | 
			
		||||
                                  @click=${this._ignoreFlow}
 | 
			
		||||
                                  .flow=${flow}
 | 
			
		||||
                                >
 | 
			
		||||
                                  ${this.hass.localize(
 | 
			
		||||
                                    "ui.panel.config.integrations.ignore.ignore"
 | 
			
		||||
                                  )}
 | 
			
		||||
                                </mwc-button>
 | 
			
		||||
                              `
 | 
			
		||||
                            : ""}
 | 
			
		||||
                        </div>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </ha-card>
 | 
			
		||||
                `
 | 
			
		||||
                    </ha-card>
 | 
			
		||||
                  `;
 | 
			
		||||
                }
 | 
			
		||||
              )
 | 
			
		||||
            : ""}
 | 
			
		||||
          ${groupedConfigEntries.size
 | 
			
		||||
@@ -639,6 +656,18 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
 | 
			
		||||
          flex-direction: column;
 | 
			
		||||
          justify-content: space-between;
 | 
			
		||||
        }
 | 
			
		||||
        .attention {
 | 
			
		||||
          --ha-card-border-color: var(--error-color);
 | 
			
		||||
        }
 | 
			
		||||
        .attention .header {
 | 
			
		||||
          background: var(--error-color);
 | 
			
		||||
          color: var(--text-primary-color);
 | 
			
		||||
          padding: 8px;
 | 
			
		||||
          text-align: center;
 | 
			
		||||
        }
 | 
			
		||||
        .attention mwc-button {
 | 
			
		||||
          --mdc-theme-primary: var(--error-color);
 | 
			
		||||
        }
 | 
			
		||||
        .discovered {
 | 
			
		||||
          --ha-card-border-color: var(--primary-color);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -137,6 +137,7 @@ export class HaIntegrationCard extends LitElement {
 | 
			
		||||
 | 
			
		||||
  private _renderSingleEntry(item: ConfigEntryExtended): TemplateResult {
 | 
			
		||||
    const devices = this._getDevices(item);
 | 
			
		||||
    const services = this._getServices(item);
 | 
			
		||||
    const entities = this._getEntities(item);
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
@@ -168,7 +169,7 @@ export class HaIntegrationCard extends LitElement {
 | 
			
		||||
          <h3>
 | 
			
		||||
            ${item.localized_domain_name === item.title ? "" : item.title}
 | 
			
		||||
          </h3>
 | 
			
		||||
          ${devices.length || entities.length
 | 
			
		||||
          ${devices.length || services.length || entities.length
 | 
			
		||||
            ? html`
 | 
			
		||||
                <div>
 | 
			
		||||
                  ${devices.length
 | 
			
		||||
@@ -180,10 +181,22 @@ export class HaIntegrationCard extends LitElement {
 | 
			
		||||
                            "count",
 | 
			
		||||
                            devices.length
 | 
			
		||||
                          )}</a
 | 
			
		||||
                        >${services.length ? "," : ""}
 | 
			
		||||
                      `
 | 
			
		||||
                    : ""}
 | 
			
		||||
                  ${services.length
 | 
			
		||||
                    ? html`
 | 
			
		||||
                        <a
 | 
			
		||||
                          href=${`/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
 | 
			
		||||
                          >${this.hass.localize(
 | 
			
		||||
                            "ui.panel.config.integrations.config_entry.services",
 | 
			
		||||
                            "count",
 | 
			
		||||
                            services.length
 | 
			
		||||
                          )}</a
 | 
			
		||||
                        >
 | 
			
		||||
                      `
 | 
			
		||||
                    : ""}
 | 
			
		||||
                  ${devices.length && entities.length
 | 
			
		||||
                  ${(devices.length || services.length) && entities.length
 | 
			
		||||
                    ? this.hass.localize("ui.common.and")
 | 
			
		||||
                    : ""}
 | 
			
		||||
                  ${entities.length
 | 
			
		||||
@@ -304,8 +317,21 @@ export class HaIntegrationCard extends LitElement {
 | 
			
		||||
    if (!this.deviceRegistryEntries) {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
    return this.deviceRegistryEntries.filter((device) =>
 | 
			
		||||
      device.config_entries.includes(configEntry.entry_id)
 | 
			
		||||
    return this.deviceRegistryEntries.filter(
 | 
			
		||||
      (device) =>
 | 
			
		||||
        device.config_entries.includes(configEntry.entry_id) &&
 | 
			
		||||
        device.entry_type !== "service"
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _getServices(configEntry: ConfigEntry): DeviceRegistryEntry[] {
 | 
			
		||||
    if (!this.deviceRegistryEntries) {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
    return this.deviceRegistryEntries.filter(
 | 
			
		||||
      (device) =>
 | 
			
		||||
        device.config_entries.includes(configEntry.entry_id) &&
 | 
			
		||||
        device.entry_type === "service"
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,9 +11,9 @@ import {
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { HASSDomEvent } from "../../../../../common/dom/fire_event";
 | 
			
		||||
import { navigate } from "../../../../../common/navigate";
 | 
			
		||||
import "../../../../../components/buttons/ha-call-service-button";
 | 
			
		||||
import { HASSDomEvent } from "../../../../../common/dom/fire_event";
 | 
			
		||||
import {
 | 
			
		||||
  DataTableColumnContainer,
 | 
			
		||||
  RowClickedEvent,
 | 
			
		||||
@@ -117,7 +117,6 @@ class OZWNetworkNodes extends LitElement {
 | 
			
		||||
        .data=${this._nodes}
 | 
			
		||||
        id="node_id"
 | 
			
		||||
        @row-click=${this._handleRowClicked}
 | 
			
		||||
        back-path="/config/ozw/network/${this.ozwInstance}/dashboard"
 | 
			
		||||
      >
 | 
			
		||||
      </hass-tabs-subpage-data-table>
 | 
			
		||||
    `;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,21 @@
 | 
			
		||||
import "@material/mwc-icon-button/mwc-icon-button";
 | 
			
		||||
import { mdiContentCopy } from "@mdi/js";
 | 
			
		||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
 | 
			
		||||
import "@polymer/paper-tooltip/paper-tooltip";
 | 
			
		||||
import type { PaperTooltipElement } from "@polymer/paper-tooltip/paper-tooltip";
 | 
			
		||||
import {
 | 
			
		||||
  css,
 | 
			
		||||
  CSSResult,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  query,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { fireEvent } from "../../../common/dom/fire_event";
 | 
			
		||||
import "../../../components/dialog/ha-paper-dialog";
 | 
			
		||||
import "../../../components/ha-svg-icon";
 | 
			
		||||
import {
 | 
			
		||||
  domainToName,
 | 
			
		||||
  fetchIntegrationManifest,
 | 
			
		||||
@@ -16,12 +23,11 @@ import {
 | 
			
		||||
  IntegrationManifest,
 | 
			
		||||
} from "../../../data/integration";
 | 
			
		||||
import { getLoggedErrorIntegration } from "../../../data/system_log";
 | 
			
		||||
import { PolymerChangedEvent } from "../../../polymer-types";
 | 
			
		||||
import type { PolymerChangedEvent } from "../../../polymer-types";
 | 
			
		||||
import { haStyleDialog } from "../../../resources/styles";
 | 
			
		||||
import { HomeAssistant } from "../../../types";
 | 
			
		||||
import { SystemLogDetailDialogParams } from "./show-dialog-system-log-detail";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import type { SystemLogDetailDialogParams } from "./show-dialog-system-log-detail";
 | 
			
		||||
import { formatSystemLogTime } from "./util";
 | 
			
		||||
import { fireEvent } from "../../../common/dom/fire_event";
 | 
			
		||||
 | 
			
		||||
class DialogSystemLogDetail extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
@@ -30,6 +36,8 @@ class DialogSystemLogDetail extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _manifest?: IntegrationManifest;
 | 
			
		||||
 | 
			
		||||
  @query("paper-tooltip") private _toolTip?: PaperTooltipElement;
 | 
			
		||||
 | 
			
		||||
  public async showDialog(params: SystemLogDetailDialogParams): Promise<void> {
 | 
			
		||||
    this._params = params;
 | 
			
		||||
    this._manifest = undefined;
 | 
			
		||||
@@ -66,13 +74,25 @@ class DialogSystemLogDetail extends LitElement {
 | 
			
		||||
        opened
 | 
			
		||||
        @opened-changed="${this._openedChanged}"
 | 
			
		||||
      >
 | 
			
		||||
        <h2>
 | 
			
		||||
          ${this.hass.localize(
 | 
			
		||||
            "ui.panel.config.logs.details",
 | 
			
		||||
            "level",
 | 
			
		||||
            item.level
 | 
			
		||||
          )}
 | 
			
		||||
        </h2>
 | 
			
		||||
        <div class="heading">
 | 
			
		||||
          <h2>
 | 
			
		||||
            ${this.hass.localize(
 | 
			
		||||
              "ui.panel.config.logs.details",
 | 
			
		||||
              "level",
 | 
			
		||||
              item.level
 | 
			
		||||
            )}
 | 
			
		||||
          </h2>
 | 
			
		||||
          <mwc-icon-button id="copy" @click=${this._copyLog}>
 | 
			
		||||
            <ha-svg-icon .path=${mdiContentCopy}></ha-svg-icon>
 | 
			
		||||
          </mwc-icon-button>
 | 
			
		||||
          <paper-tooltip
 | 
			
		||||
            manual-mode
 | 
			
		||||
            for="copy"
 | 
			
		||||
            position="top"
 | 
			
		||||
            animation-delay="0"
 | 
			
		||||
            >${this.hass.localize("ui.common.copied")}</paper-tooltip
 | 
			
		||||
          >
 | 
			
		||||
        </div>
 | 
			
		||||
        <paper-dialog-scrollable>
 | 
			
		||||
          <p>
 | 
			
		||||
            Logger: ${item.name}<br />
 | 
			
		||||
@@ -148,6 +168,25 @@ class DialogSystemLogDetail extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _copyLog(): void {
 | 
			
		||||
    const copyElement = this.shadowRoot?.querySelector(
 | 
			
		||||
      "paper-dialog-scrollable"
 | 
			
		||||
    ) as HTMLElement;
 | 
			
		||||
 | 
			
		||||
    const selection = window.getSelection()!;
 | 
			
		||||
    const range = document.createRange();
 | 
			
		||||
 | 
			
		||||
    range.selectNodeContents(copyElement);
 | 
			
		||||
    selection.removeAllRanges();
 | 
			
		||||
    selection.addRange(range);
 | 
			
		||||
 | 
			
		||||
    document.execCommand("copy");
 | 
			
		||||
    window.getSelection()!.removeAllRanges();
 | 
			
		||||
 | 
			
		||||
    this._toolTip!.show();
 | 
			
		||||
    setTimeout(() => this._toolTip?.hide(), 3000);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get styles(): CSSResult[] {
 | 
			
		||||
    return [
 | 
			
		||||
      haStyleDialog,
 | 
			
		||||
@@ -164,6 +203,15 @@ class DialogSystemLogDetail extends LitElement {
 | 
			
		||||
        pre {
 | 
			
		||||
          margin-bottom: 0;
 | 
			
		||||
        }
 | 
			
		||||
        .heading {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          display: flex;
 | 
			
		||||
          align-items: center;
 | 
			
		||||
          justify-content: space-between;
 | 
			
		||||
        }
 | 
			
		||||
        .heading ha-svg-icon {
 | 
			
		||||
          cursor: pointer;
 | 
			
		||||
        }
 | 
			
		||||
      `,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -198,6 +198,7 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
 | 
			
		||||
          ? ""
 | 
			
		||||
          : html`
 | 
			
		||||
              <ha-icon-button
 | 
			
		||||
                class="warning"
 | 
			
		||||
                slot="toolbar-icon"
 | 
			
		||||
                title="${this.hass.localize(
 | 
			
		||||
                  "ui.panel.config.scene.picker.delete_scene"
 | 
			
		||||
 
 | 
			
		||||
@@ -18,12 +18,14 @@ import { navigate } from "../../../common/navigate";
 | 
			
		||||
import { computeRTL } from "../../../common/util/compute_rtl";
 | 
			
		||||
import "../../../components/ha-card";
 | 
			
		||||
import "../../../components/ha-icon-input";
 | 
			
		||||
import { showToast } from "../../../util/toast";
 | 
			
		||||
import "@material/mwc-fab";
 | 
			
		||||
import {
 | 
			
		||||
  Action,
 | 
			
		||||
  deleteScript,
 | 
			
		||||
  getScriptEditorInitData,
 | 
			
		||||
  ScriptConfig,
 | 
			
		||||
  triggerScript,
 | 
			
		||||
  MODES,
 | 
			
		||||
  MODES_MAX,
 | 
			
		||||
} from "../../../data/script";
 | 
			
		||||
@@ -74,6 +76,7 @@ export class HaScriptEditor extends LitElement {
 | 
			
		||||
          ? ""
 | 
			
		||||
          : html`
 | 
			
		||||
              <ha-icon-button
 | 
			
		||||
                class="warning"
 | 
			
		||||
                slot="toolbar-icon"
 | 
			
		||||
                title="${this.hass.localize(
 | 
			
		||||
                  "ui.panel.config.script.editor.delete_script"
 | 
			
		||||
@@ -192,6 +195,22 @@ export class HaScriptEditor extends LitElement {
 | 
			
		||||
                            </paper-input>`
 | 
			
		||||
                          : html``}
 | 
			
		||||
                      </div>
 | 
			
		||||
                      ${this.scriptEntityId
 | 
			
		||||
                        ? html`
 | 
			
		||||
                          <div class="card-actions layout horizontal justified center">
 | 
			
		||||
                            <span></span>
 | 
			
		||||
                            <mwc-button
 | 
			
		||||
                              @click=${this._runScript}
 | 
			
		||||
                              title="${this.hass.localize(
 | 
			
		||||
                                "ui.panel.config.script.picker.activate_script"
 | 
			
		||||
                             )}"
 | 
			
		||||
                             ?disabled=${this._dirty}
 | 
			
		||||
                            >
 | 
			
		||||
                              ${this.hass.localize("ui.card.script.execute")}
 | 
			
		||||
                           </mwc-button>
 | 
			
		||||
                         </div>
 | 
			
		||||
                          `
 | 
			
		||||
                        : ``}
 | 
			
		||||
                    </ha-card>
 | 
			
		||||
                  </ha-config-section>
 | 
			
		||||
 | 
			
		||||
@@ -300,6 +319,18 @@ export class HaScriptEditor extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _runScript(ev) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    await triggerScript(this.hass, this.scriptEntityId);
 | 
			
		||||
    showToast(this, {
 | 
			
		||||
      message: this.hass.localize(
 | 
			
		||||
        "ui.notification_toast.triggered",
 | 
			
		||||
        "name",
 | 
			
		||||
        this._config!.alias
 | 
			
		||||
      ),
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _modeChanged(ev: CustomEvent) {
 | 
			
		||||
    const mode = ((ev.target as PaperListboxElement)?.selectedItem as any)
 | 
			
		||||
      ?.mode;
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ import {
 | 
			
		||||
  property,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { isServiceLoaded } from "../../../common/config/is_service_loaded";
 | 
			
		||||
import { componentsWithService } from "../../../common/config/components_with_service";
 | 
			
		||||
import "../../../components/buttons/ha-call-service-button";
 | 
			
		||||
import "../../../components/ha-card";
 | 
			
		||||
import { checkCoreConfig } from "../../../data/core";
 | 
			
		||||
@@ -49,11 +49,10 @@ export class HaConfigServerControl extends LitElement {
 | 
			
		||||
      changedProperties.has("hass") &&
 | 
			
		||||
      (!oldHass || oldHass.config.components !== this.hass.config.components)
 | 
			
		||||
    ) {
 | 
			
		||||
      this._reloadableDomains = this.hass.config.components.filter(
 | 
			
		||||
        (component) =>
 | 
			
		||||
          !component.includes(".") &&
 | 
			
		||||
          isServiceLoaded(this.hass, component, "reload")
 | 
			
		||||
      );
 | 
			
		||||
      this._reloadableDomains = componentsWithService(
 | 
			
		||||
        this.hass,
 | 
			
		||||
        "reload"
 | 
			
		||||
      ).sort();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -203,24 +202,23 @@ export class HaConfigServerControl extends LitElement {
 | 
			
		||||
                      )}
 | 
			
		||||
                    </ha-call-service-button>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  ${this._reloadableDomains.map((domain) =>
 | 
			
		||||
                    isServiceLoaded(this.hass, domain, "reload")
 | 
			
		||||
                      ? html`<div class="card-actions">
 | 
			
		||||
                          <ha-call-service-button
 | 
			
		||||
                            .hass=${this.hass}
 | 
			
		||||
                            .domain=${domain}
 | 
			
		||||
                            service="reload"
 | 
			
		||||
                            >${this.hass.localize(
 | 
			
		||||
                              `ui.panel.config.server_control.section.reloading.${domain}`
 | 
			
		||||
                            ) ||
 | 
			
		||||
                            this.hass.localize(
 | 
			
		||||
                              "ui.panel.config.server_control.section.reloading.reload",
 | 
			
		||||
                              "domain",
 | 
			
		||||
                              domainToName(this.hass.localize, domain)
 | 
			
		||||
                            )}
 | 
			
		||||
                          </ha-call-service-button>
 | 
			
		||||
                        </div>`
 | 
			
		||||
                      : ""
 | 
			
		||||
                  ${this._reloadableDomains.map(
 | 
			
		||||
                    (domain) =>
 | 
			
		||||
                      html`<div class="card-actions">
 | 
			
		||||
                        <ha-call-service-button
 | 
			
		||||
                          .hass=${this.hass}
 | 
			
		||||
                          .domain=${domain}
 | 
			
		||||
                          service="reload"
 | 
			
		||||
                          >${this.hass.localize(
 | 
			
		||||
                            `ui.panel.config.server_control.section.reloading.${domain}`
 | 
			
		||||
                          ) ||
 | 
			
		||||
                          this.hass.localize(
 | 
			
		||||
                            "ui.panel.config.server_control.section.reloading.reload",
 | 
			
		||||
                            "domain",
 | 
			
		||||
                            domainToName(this.hass.localize, domain)
 | 
			
		||||
                          )}
 | 
			
		||||
                        </ha-call-service-button>
 | 
			
		||||
                      </div>`
 | 
			
		||||
                  )}
 | 
			
		||||
                </ha-card>
 | 
			
		||||
              `
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
import "@material/mwc-fab";
 | 
			
		||||
import { mdiPlus } from "@mdi/js";
 | 
			
		||||
import {
 | 
			
		||||
  customElement,
 | 
			
		||||
  LitElement,
 | 
			
		||||
@@ -11,7 +13,7 @@ import {
 | 
			
		||||
  DataTableColumnContainer,
 | 
			
		||||
  RowClickedEvent,
 | 
			
		||||
} from "../../../components/data-table/ha-data-table";
 | 
			
		||||
import "@material/mwc-fab";
 | 
			
		||||
import "../../../components/ha-svg-icon";
 | 
			
		||||
import { deleteUser, fetchUsers, updateUser, User } from "../../../data/user";
 | 
			
		||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
 | 
			
		||||
import "../../../layouts/hass-tabs-subpage-data-table";
 | 
			
		||||
@@ -19,8 +21,6 @@ import { HomeAssistant, Route } from "../../../types";
 | 
			
		||||
import { configSections } from "../ha-panel-config";
 | 
			
		||||
import { showAddUserDialog } from "./show-dialog-add-user";
 | 
			
		||||
import { showUserDetailDialog } from "./show-dialog-user-detail";
 | 
			
		||||
import "../../../components/ha-svg-icon";
 | 
			
		||||
import { mdiPlus } from "@mdi/js";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-config-users")
 | 
			
		||||
export class HaConfigUsers extends LitElement {
 | 
			
		||||
@@ -56,7 +56,7 @@ export class HaConfigUsers extends LitElement {
 | 
			
		||||
          ),
 | 
			
		||||
          sortable: true,
 | 
			
		||||
          filterable: true,
 | 
			
		||||
          width: "25%",
 | 
			
		||||
          width: "30%",
 | 
			
		||||
          template: (groupIds) => html`
 | 
			
		||||
            ${this.hass.localize(`groups.${groupIds[0]}`)}
 | 
			
		||||
          `,
 | 
			
		||||
@@ -66,6 +66,7 @@ export class HaConfigUsers extends LitElement {
 | 
			
		||||
            "ui.panel.config.users.picker.headers.system"
 | 
			
		||||
          ),
 | 
			
		||||
          type: "icon",
 | 
			
		||||
          width: "80px",
 | 
			
		||||
          sortable: true,
 | 
			
		||||
          filterable: true,
 | 
			
		||||
          template: (generated) => html`
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,10 @@ import { classMap } from "lit-html/directives/class-map";
 | 
			
		||||
import { debounce } from "../../../common/util/debounce";
 | 
			
		||||
import "../../../components/ha-circular-progress";
 | 
			
		||||
import "../../../components/ha-code-editor";
 | 
			
		||||
import { subscribeRenderTemplate } from "../../../data/ws-templates";
 | 
			
		||||
import {
 | 
			
		||||
  RenderTemplateResult,
 | 
			
		||||
  subscribeRenderTemplate,
 | 
			
		||||
} from "../../../data/ws-templates";
 | 
			
		||||
import { haStyle } from "../../../resources/styles";
 | 
			
		||||
import { HomeAssistant } from "../../../types";
 | 
			
		||||
 | 
			
		||||
@@ -31,10 +34,9 @@ The temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}.
 | 
			
		||||
  The sun will rise at {{ as_timestamp(strptime(state_attr("sun.sun", "next_rising"), "")) | timestamp_local }}.
 | 
			
		||||
{%- endif %}
 | 
			
		||||
 | 
			
		||||
For loop example getting 3 entity values:
 | 
			
		||||
For loop example getting entity values in the weather domain:
 | 
			
		||||
 | 
			
		||||
{% for states in states | slice(3) -%}
 | 
			
		||||
  {% set state = states | first %}
 | 
			
		||||
{% for state in states.weather -%}
 | 
			
		||||
  {%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}
 | 
			
		||||
  {{ state.name | lower }} is {{state.state_with_unit}}
 | 
			
		||||
{%- endfor %}.`;
 | 
			
		||||
@@ -45,11 +47,11 @@ class HaPanelDevTemplate extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @property() public narrow!: boolean;
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _error = false;
 | 
			
		||||
  @internalProperty() private _error?: string;
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _rendering = false;
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _processed = "";
 | 
			
		||||
  @internalProperty() private _templateResult?: RenderTemplateResult;
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _unsubRenderTemplate?: Promise<UnsubscribeFunc>;
 | 
			
		||||
 | 
			
		||||
@@ -140,9 +142,65 @@ class HaPanelDevTemplate extends LitElement {
 | 
			
		||||
            .active=${this._rendering}
 | 
			
		||||
            size="small"
 | 
			
		||||
          ></ha-circular-progress>
 | 
			
		||||
          <pre class="rendered ${classMap({ error: this._error })}">
 | 
			
		||||
${this._processed}</pre
 | 
			
		||||
          >
 | 
			
		||||
 | 
			
		||||
          <pre
 | 
			
		||||
            class="rendered ${classMap({ error: Boolean(this._error) })}"
 | 
			
		||||
          ><!-- display: block -->${this._error}${this._templateResult
 | 
			
		||||
            ?.result}</pre>
 | 
			
		||||
          ${!this._templateResult?.listeners
 | 
			
		||||
            ? ""
 | 
			
		||||
            : this._templateResult.listeners.all
 | 
			
		||||
            ? html`
 | 
			
		||||
                <h3 class="all_listeners">
 | 
			
		||||
                  ${this.hass.localize(
 | 
			
		||||
                    "ui.panel.developer-tools.tabs.templates.all_listeners"
 | 
			
		||||
                  )}
 | 
			
		||||
                </h3>
 | 
			
		||||
              `
 | 
			
		||||
            : this._templateResult.listeners.domains.length ||
 | 
			
		||||
              this._templateResult.listeners.entities.length
 | 
			
		||||
            ? html`
 | 
			
		||||
                <h3>
 | 
			
		||||
                  ${this.hass.localize(
 | 
			
		||||
                    "ui.panel.developer-tools.tabs.templates.listeners"
 | 
			
		||||
                  )}
 | 
			
		||||
                </h3>
 | 
			
		||||
                <ul>
 | 
			
		||||
                  ${this._templateResult.listeners.domains
 | 
			
		||||
                    .sort()
 | 
			
		||||
                    .map(
 | 
			
		||||
                      (domain) =>
 | 
			
		||||
                        html`
 | 
			
		||||
                          <li>
 | 
			
		||||
                            <b
 | 
			
		||||
                              >${this.hass.localize(
 | 
			
		||||
                                "ui.panel.developer-tools.tabs.templates.domain"
 | 
			
		||||
                              )}</b
 | 
			
		||||
                            >: ${domain}
 | 
			
		||||
                          </li>
 | 
			
		||||
                        `
 | 
			
		||||
                    )}
 | 
			
		||||
                  ${this._templateResult.listeners.entities
 | 
			
		||||
                    .sort()
 | 
			
		||||
                    .map(
 | 
			
		||||
                      (entity_id) =>
 | 
			
		||||
                        html`
 | 
			
		||||
                          <li>
 | 
			
		||||
                            <b
 | 
			
		||||
                              >${this.hass.localize(
 | 
			
		||||
                                "ui.panel.developer-tools.tabs.templates.entity"
 | 
			
		||||
                              )}</b
 | 
			
		||||
                            >: ${entity_id}
 | 
			
		||||
                          </li>
 | 
			
		||||
                        `
 | 
			
		||||
                    )}
 | 
			
		||||
                </ul>
 | 
			
		||||
              `
 | 
			
		||||
            : html` <span class="all_listeners">
 | 
			
		||||
                ${this.hass.localize(
 | 
			
		||||
                  "ui.panel.developer-tools.tabs.templates.no_listeners"
 | 
			
		||||
                )}
 | 
			
		||||
              </span>`}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    `;
 | 
			
		||||
@@ -190,6 +248,12 @@ ${this._processed}</pre
 | 
			
		||||
          @apply --paper-font-code1;
 | 
			
		||||
          clear: both;
 | 
			
		||||
          white-space: pre-wrap;
 | 
			
		||||
          background-color: var(--secondary-background-color);
 | 
			
		||||
          padding: 8px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .all_listeners {
 | 
			
		||||
          color: var(--warning-color);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .rendered.error {
 | 
			
		||||
@@ -211,7 +275,7 @@ ${this._processed}</pre
 | 
			
		||||
  private _templateChanged(ev) {
 | 
			
		||||
    this._template = ev.detail.value;
 | 
			
		||||
    if (this._error) {
 | 
			
		||||
      this._error = false;
 | 
			
		||||
      this._error = undefined;
 | 
			
		||||
    }
 | 
			
		||||
    this._debounceRender();
 | 
			
		||||
  }
 | 
			
		||||
@@ -223,7 +287,8 @@ ${this._processed}</pre
 | 
			
		||||
      this._unsubRenderTemplate = subscribeRenderTemplate(
 | 
			
		||||
        this.hass.connection,
 | 
			
		||||
        (result) => {
 | 
			
		||||
          this._processed = result;
 | 
			
		||||
          this._templateResult = result;
 | 
			
		||||
          this._error = undefined;
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          template: this._template,
 | 
			
		||||
@@ -231,9 +296,10 @@ ${this._processed}</pre
 | 
			
		||||
      );
 | 
			
		||||
      await this._unsubRenderTemplate;
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      this._error = true;
 | 
			
		||||
      this._error = "Unknown error";
 | 
			
		||||
      if (err.message) {
 | 
			
		||||
        this._processed = err.message;
 | 
			
		||||
        this._error = err.message;
 | 
			
		||||
        this._templateResult = undefined;
 | 
			
		||||
      }
 | 
			
		||||
      this._unsubRenderTemplate = undefined;
 | 
			
		||||
    } finally {
 | 
			
		||||
 
 | 
			
		||||
@@ -79,10 +79,12 @@ class HaPanelHistory extends LitElement {
 | 
			
		||||
            ></ha-date-range-picker>
 | 
			
		||||
          </div>
 | 
			
		||||
          ${this._isLoading
 | 
			
		||||
            ? html`<ha-circular-progress
 | 
			
		||||
                active
 | 
			
		||||
                alt=${this.hass.localize("ui.common.loading")}
 | 
			
		||||
              ></ha-circular-progress>`
 | 
			
		||||
            ? html`<div class="progress-wrapper">
 | 
			
		||||
                <ha-circular-progress
 | 
			
		||||
                  active
 | 
			
		||||
                  alt=${this.hass.localize("ui.common.loading")}
 | 
			
		||||
                ></ha-circular-progress>
 | 
			
		||||
              </div>`
 | 
			
		||||
            : html`
 | 
			
		||||
                <state-history-charts
 | 
			
		||||
                  .hass=${this.hass}
 | 
			
		||||
@@ -196,6 +198,19 @@ class HaPanelHistory extends LitElement {
 | 
			
		||||
        .content {
 | 
			
		||||
          padding: 0 16px 16px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .progress-wrapper {
 | 
			
		||||
          height: calc(100vh - 136px);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host([narrow]) .progress-wrapper {
 | 
			
		||||
          height: calc(100vh - 198px);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .progress-wrapper {
 | 
			
		||||
          position: relative;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ha-circular-progress {
 | 
			
		||||
          position: absolute;
 | 
			
		||||
          left: 50%;
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,6 @@ import { computeRTL, emitRTLDirection } from "../../common/util/compute_rtl";
 | 
			
		||||
import "../../components/ha-circular-progress";
 | 
			
		||||
import "../../components/ha-icon";
 | 
			
		||||
import { LogbookEntry } from "../../data/logbook";
 | 
			
		||||
import { haStyleScrollbar } from "../../resources/styles";
 | 
			
		||||
import { HomeAssistant } from "../../types";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-logbook")
 | 
			
		||||
@@ -38,6 +37,9 @@ class HaLogbook extends LitElement {
 | 
			
		||||
  @property({ attribute: "rtl", type: Boolean })
 | 
			
		||||
  private _rtl = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, attribute: "virtualize", reflect: true })
 | 
			
		||||
  public virtualize = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, attribute: "no-icon" })
 | 
			
		||||
  public noIcon = false;
 | 
			
		||||
 | 
			
		||||
@@ -67,14 +69,14 @@ class HaLogbook extends LitElement {
 | 
			
		||||
    if (!this.entries?.length) {
 | 
			
		||||
      return html`
 | 
			
		||||
        <div class="container no-entries" .dir=${emitRTLDirection(this._rtl)}>
 | 
			
		||||
          ${this.hass.localize("ui.panel.logbook.entries_not_found")}
 | 
			
		||||
          ${this.hass.localize("ui.components.logbook.entries_not_found")}
 | 
			
		||||
        </div>
 | 
			
		||||
      `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <div
 | 
			
		||||
        class="container ha-scrollbar ${classMap({
 | 
			
		||||
        class="container ${classMap({
 | 
			
		||||
          narrow: this.narrow,
 | 
			
		||||
          rtl: this._rtl,
 | 
			
		||||
          "no-name": this.noName,
 | 
			
		||||
@@ -82,11 +84,15 @@ class HaLogbook extends LitElement {
 | 
			
		||||
        })}"
 | 
			
		||||
        @scroll=${this._saveScrollPos}
 | 
			
		||||
      >
 | 
			
		||||
        ${scroll({
 | 
			
		||||
          items: this.entries,
 | 
			
		||||
          renderItem: (item: LogbookEntry, index?: number) =>
 | 
			
		||||
            this._renderLogbookItem(item, index),
 | 
			
		||||
        })}
 | 
			
		||||
        ${this.virtualize
 | 
			
		||||
          ? scroll({
 | 
			
		||||
              items: this.entries,
 | 
			
		||||
              renderItem: (item: LogbookEntry, index?: number) =>
 | 
			
		||||
                this._renderLogbookItem(item, index),
 | 
			
		||||
            })
 | 
			
		||||
          : this.entries.map((item, index) =>
 | 
			
		||||
              this._renderLogbookItem(item, index)
 | 
			
		||||
            )}
 | 
			
		||||
      </div>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
@@ -143,20 +149,23 @@ class HaLogbook extends LitElement {
 | 
			
		||||
                      >
 | 
			
		||||
                    `
 | 
			
		||||
                : ""}
 | 
			
		||||
              <span class="item-message">${item.message}</span>
 | 
			
		||||
              <span>${item_username ? ` (${item_username})` : ``}</span>
 | 
			
		||||
              ${!item.context_event_type
 | 
			
		||||
              ${item.message}
 | 
			
		||||
              ${item_username
 | 
			
		||||
                ? ` by ${item_username}`
 | 
			
		||||
                : !item.context_event_type
 | 
			
		||||
                ? ""
 | 
			
		||||
                : item.context_event_type === "call_service"
 | 
			
		||||
                ? // Service Call
 | 
			
		||||
                  html` by service
 | 
			
		||||
                  ` by service
 | 
			
		||||
                  ${item.context_domain}.${item.context_service}`
 | 
			
		||||
                : item.context_entity_id === item.entity_id
 | 
			
		||||
                ? // HomeKit or something that self references
 | 
			
		||||
                  html` by
 | 
			
		||||
                  ${item.context_name
 | 
			
		||||
                    ? item.context_name
 | 
			
		||||
                    : item.context_event_type}`
 | 
			
		||||
                  ` by
 | 
			
		||||
                  ${
 | 
			
		||||
                    item.context_name
 | 
			
		||||
                      ? item.context_name
 | 
			
		||||
                      : item.context_event_type
 | 
			
		||||
                  }`
 | 
			
		||||
                : // Another entity such as an automation or script
 | 
			
		||||
                  html` by
 | 
			
		||||
                    <a
 | 
			
		||||
@@ -185,106 +194,104 @@ class HaLogbook extends LitElement {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get styles(): CSSResult[] {
 | 
			
		||||
    return [
 | 
			
		||||
      haStyleScrollbar,
 | 
			
		||||
      css`
 | 
			
		||||
        :host {
 | 
			
		||||
          display: block;
 | 
			
		||||
          height: 100%;
 | 
			
		||||
        }
 | 
			
		||||
  static get styles(): CSSResult {
 | 
			
		||||
    return css`
 | 
			
		||||
      :host {
 | 
			
		||||
        display: block;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        .rtl {
 | 
			
		||||
          direction: ltr;
 | 
			
		||||
        }
 | 
			
		||||
      .rtl {
 | 
			
		||||
        direction: ltr;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        .entry-container {
 | 
			
		||||
          width: 100%;
 | 
			
		||||
        }
 | 
			
		||||
      .entry-container {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        .entry {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          width: 100%;
 | 
			
		||||
          line-height: 2em;
 | 
			
		||||
          padding: 8px 16px;
 | 
			
		||||
          box-sizing: border-box;
 | 
			
		||||
          border-top: 1px solid
 | 
			
		||||
            var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
 | 
			
		||||
        }
 | 
			
		||||
      .entry {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        line-height: 2em;
 | 
			
		||||
        padding: 8px 16px;
 | 
			
		||||
        box-sizing: border-box;
 | 
			
		||||
        border-top: 1px solid
 | 
			
		||||
          var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        .time {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          justify-content: center;
 | 
			
		||||
          flex-direction: column;
 | 
			
		||||
          width: 65px;
 | 
			
		||||
          flex-shrink: 0;
 | 
			
		||||
          font-size: 12px;
 | 
			
		||||
          color: var(--secondary-text-color);
 | 
			
		||||
        }
 | 
			
		||||
      .time {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        width: 75px;
 | 
			
		||||
        flex-shrink: 0;
 | 
			
		||||
        font-size: 12px;
 | 
			
		||||
        color: var(--secondary-text-color);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        .date {
 | 
			
		||||
          margin: 8px 0;
 | 
			
		||||
          padding: 0 16px;
 | 
			
		||||
        }
 | 
			
		||||
      .date {
 | 
			
		||||
        margin: 8px 0;
 | 
			
		||||
        padding: 0 16px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        .narrow .date {
 | 
			
		||||
          padding: 0 8px;
 | 
			
		||||
        }
 | 
			
		||||
      .narrow .date {
 | 
			
		||||
        padding: 0 8px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        .rtl .date {
 | 
			
		||||
          direction: rtl;
 | 
			
		||||
        }
 | 
			
		||||
      .rtl .date {
 | 
			
		||||
        direction: rtl;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        .icon-message {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          align-items: center;
 | 
			
		||||
        }
 | 
			
		||||
      .icon-message {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        .no-entries {
 | 
			
		||||
          text-align: center;
 | 
			
		||||
        }
 | 
			
		||||
      .no-entries {
 | 
			
		||||
        text-align: center;
 | 
			
		||||
        color: var(--secondary-text-color);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        ha-icon {
 | 
			
		||||
          margin: 0 8px 0 16px;
 | 
			
		||||
          flex-shrink: 0;
 | 
			
		||||
          color: var(--primary-text-color);
 | 
			
		||||
        }
 | 
			
		||||
      ha-icon {
 | 
			
		||||
        margin: 0 8px 0 16px;
 | 
			
		||||
        flex-shrink: 0;
 | 
			
		||||
        color: var(--primary-text-color);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        .message {
 | 
			
		||||
          color: var(--primary-text-color);
 | 
			
		||||
        }
 | 
			
		||||
      .message {
 | 
			
		||||
        color: var(--primary-text-color);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        .no-name .item-message {
 | 
			
		||||
          text-transform: capitalize;
 | 
			
		||||
        }
 | 
			
		||||
      .no-name .message:first-letter {
 | 
			
		||||
        text-transform: capitalize;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        a {
 | 
			
		||||
          color: var(--primary-color);
 | 
			
		||||
        }
 | 
			
		||||
      a {
 | 
			
		||||
        color: var(--primary-color);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        .uni-virtualizer-host {
 | 
			
		||||
          display: block;
 | 
			
		||||
          position: relative;
 | 
			
		||||
          contain: strict;
 | 
			
		||||
          height: 100%;
 | 
			
		||||
          overflow: auto;
 | 
			
		||||
        }
 | 
			
		||||
      .uni-virtualizer-host {
 | 
			
		||||
        display: block;
 | 
			
		||||
        position: relative;
 | 
			
		||||
        contain: strict;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
        overflow: auto;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        .uni-virtualizer-host > * {
 | 
			
		||||
          box-sizing: border-box;
 | 
			
		||||
        }
 | 
			
		||||
      .uni-virtualizer-host > * {
 | 
			
		||||
        box-sizing: border-box;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        .narrow .entry {
 | 
			
		||||
          flex-direction: column;
 | 
			
		||||
          line-height: 1.5;
 | 
			
		||||
          padding: 8px;
 | 
			
		||||
        }
 | 
			
		||||
      .narrow .entry {
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        line-height: 1.5;
 | 
			
		||||
        padding: 8px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        .narrow .icon-message ha-icon {
 | 
			
		||||
          margin-left: 0;
 | 
			
		||||
        }
 | 
			
		||||
      `,
 | 
			
		||||
    ];
 | 
			
		||||
      .narrow .icon-message ha-icon {
 | 
			
		||||
        margin-left: 0;
 | 
			
		||||
      }
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,33 +1,33 @@
 | 
			
		||||
import { mdiRefresh } from "@mdi/js";
 | 
			
		||||
import "@polymer/app-layout/app-header/app-header";
 | 
			
		||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
 | 
			
		||||
import "../../components/ha-icon-button";
 | 
			
		||||
import "../../components/ha-circular-progress";
 | 
			
		||||
import { computeRTL } from "../../common/util/compute_rtl";
 | 
			
		||||
import "../../components/entity/ha-entity-picker";
 | 
			
		||||
import "../../components/ha-menu-button";
 | 
			
		||||
import "../../layouts/ha-app-layout";
 | 
			
		||||
import "./ha-logbook";
 | 
			
		||||
import {
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  css,
 | 
			
		||||
  customElement,
 | 
			
		||||
  html,
 | 
			
		||||
  css,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  PropertyValues,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
import { HomeAssistant } from "../../types";
 | 
			
		||||
import { haStyle } from "../../resources/styles";
 | 
			
		||||
import { fetchUsers } from "../../data/user";
 | 
			
		||||
import { fetchPersons } from "../../data/person";
 | 
			
		||||
import { computeRTL } from "../../common/util/compute_rtl";
 | 
			
		||||
import "../../components/entity/ha-entity-picker";
 | 
			
		||||
import "../../components/ha-circular-progress";
 | 
			
		||||
import "../../components/ha-date-range-picker";
 | 
			
		||||
import type { DateRangePickerRanges } from "../../components/ha-date-range-picker";
 | 
			
		||||
import "../../components/ha-icon-button";
 | 
			
		||||
import "../../components/ha-menu-button";
 | 
			
		||||
import {
 | 
			
		||||
  clearLogbookCache,
 | 
			
		||||
  getLogbookData,
 | 
			
		||||
  LogbookEntry,
 | 
			
		||||
} from "../../data/logbook";
 | 
			
		||||
import { mdiRefresh } from "@mdi/js";
 | 
			
		||||
import "../../components/ha-date-range-picker";
 | 
			
		||||
import type { DateRangePickerRanges } from "../../components/ha-date-range-picker";
 | 
			
		||||
import { fetchPersons } from "../../data/person";
 | 
			
		||||
import { fetchUsers } from "../../data/user";
 | 
			
		||||
import "../../layouts/ha-app-layout";
 | 
			
		||||
import { haStyle } from "../../resources/styles";
 | 
			
		||||
import { HomeAssistant } from "../../types";
 | 
			
		||||
import "./ha-logbook";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-panel-logbook")
 | 
			
		||||
export class HaPanelLogbook extends LitElement {
 | 
			
		||||
@@ -125,6 +125,7 @@ export class HaPanelLogbook extends LitElement {
 | 
			
		||||
              .hass=${this.hass}
 | 
			
		||||
              .entries=${this._entries}
 | 
			
		||||
              .userIdToName=${this._userIdToName}
 | 
			
		||||
              virtualize
 | 
			
		||||
            ></ha-logbook>`}
 | 
			
		||||
      </ha-app-layout>
 | 
			
		||||
    `;
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ import { DOMAINS_TOGGLE } from "../../../common/const";
 | 
			
		||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
 | 
			
		||||
import { computeActiveState } from "../../../common/entity/compute_active_state";
 | 
			
		||||
import { computeDomain } from "../../../common/entity/compute_domain";
 | 
			
		||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
 | 
			
		||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
 | 
			
		||||
import { computeStateName } from "../../../common/entity/compute_state_name";
 | 
			
		||||
import { stateIcon } from "../../../common/entity/state_icon";
 | 
			
		||||
@@ -36,7 +37,6 @@ import { hasAction } from "../common/has-action";
 | 
			
		||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
 | 
			
		||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
 | 
			
		||||
import { ButtonCardConfig } from "./types";
 | 
			
		||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
 | 
			
		||||
 | 
			
		||||
@customElement("hui-button-card")
 | 
			
		||||
export class HuiButtonCard extends LitElement implements LovelaceCard {
 | 
			
		||||
@@ -63,11 +63,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      type: "button",
 | 
			
		||||
      tap_action: { action: "toggle" },
 | 
			
		||||
      hold_action: { action: "more-info" },
 | 
			
		||||
      show_icon: true,
 | 
			
		||||
      show_name: true,
 | 
			
		||||
      show_state: false,
 | 
			
		||||
      entity: foundEntities[0] || "",
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
@@ -92,29 +87,18 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this._config = {
 | 
			
		||||
      tap_action: {
 | 
			
		||||
        action:
 | 
			
		||||
          config.entity && DOMAINS_TOGGLE.has(computeDomain(config.entity))
 | 
			
		||||
            ? "toggle"
 | 
			
		||||
            : "more-info",
 | 
			
		||||
      },
 | 
			
		||||
      hold_action: { action: "more-info" },
 | 
			
		||||
      double_tap_action: { action: "none" },
 | 
			
		||||
      show_icon: true,
 | 
			
		||||
      show_name: true,
 | 
			
		||||
      state_color: true,
 | 
			
		||||
      ...config,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (config.entity && DOMAINS_TOGGLE.has(computeDomain(config.entity))) {
 | 
			
		||||
      this._config = {
 | 
			
		||||
        tap_action: {
 | 
			
		||||
          action: "toggle",
 | 
			
		||||
        },
 | 
			
		||||
        ...this._config,
 | 
			
		||||
      };
 | 
			
		||||
    } else {
 | 
			
		||||
      this._config = {
 | 
			
		||||
        tap_action: {
 | 
			
		||||
          action: "more-info",
 | 
			
		||||
        },
 | 
			
		||||
        ...this._config,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected shouldUpdate(changedProps: PropertyValues): boolean {
 | 
			
		||||
 
 | 
			
		||||
@@ -76,11 +76,11 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
 | 
			
		||||
  private _resizeObserver?: ResizeObserver;
 | 
			
		||||
 | 
			
		||||
  public setConfig(config: CalendarCardConfig): void {
 | 
			
		||||
    if (!config.entities) {
 | 
			
		||||
    if (!config.entities?.length) {
 | 
			
		||||
      throw new Error("Entities must be defined");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.entities && !Array.isArray(config.entities)) {
 | 
			
		||||
    if (!Array.isArray(config.entities)) {
 | 
			
		||||
      throw new Error("Entities need to be an array");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
 | 
			
		||||
      ["light", "switch", "sensor"]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return { type: "entities", title: "My Title", entities: foundEntities };
 | 
			
		||||
    return { type: "entities", entities: foundEntities };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @internalProperty() private _config?: EntitiesCardConfig;
 | 
			
		||||
 
 | 
			
		||||
@@ -43,8 +43,8 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
 | 
			
		||||
    return css`
 | 
			
		||||
      :host {
 | 
			
		||||
        display: block;
 | 
			
		||||
        background-color: #ef5350;
 | 
			
		||||
        color: white;
 | 
			
		||||
        background-color: var(--error-color);
 | 
			
		||||
        color: var(--color-on-error, white);
 | 
			
		||||
        padding: 8px;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        user-select: text;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,9 @@ import {
 | 
			
		||||
  CSSResult,
 | 
			
		||||
  customElement,
 | 
			
		||||
  html,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  LitElement,
 | 
			
		||||
  property,
 | 
			
		||||
  internalProperty,
 | 
			
		||||
  PropertyValues,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit-element";
 | 
			
		||||
@@ -20,18 +20,22 @@ import "../../../components/entity/state-badge";
 | 
			
		||||
import "../../../components/ha-card";
 | 
			
		||||
import "../../../components/ha-icon";
 | 
			
		||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
 | 
			
		||||
import { ActionHandlerEvent } from "../../../data/lovelace";
 | 
			
		||||
import {
 | 
			
		||||
  ActionHandlerEvent,
 | 
			
		||||
  CallServiceActionConfig,
 | 
			
		||||
  MoreInfoActionConfig,
 | 
			
		||||
} from "../../../data/lovelace";
 | 
			
		||||
import { HomeAssistant } from "../../../types";
 | 
			
		||||
import { actionHandler } from "../common/directives/action-handler-directive";
 | 
			
		||||
import { findEntities } from "../common/find-entites";
 | 
			
		||||
import { handleAction } from "../common/handle-action";
 | 
			
		||||
import { hasAction } from "../common/has-action";
 | 
			
		||||
import { processConfigEntities } from "../common/process-config-entities";
 | 
			
		||||
import "../components/hui-timestamp-display";
 | 
			
		||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
 | 
			
		||||
import "../components/hui-warning-element";
 | 
			
		||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
 | 
			
		||||
import "../components/hui-timestamp-display";
 | 
			
		||||
import { GlanceCardConfig, GlanceConfigEntity } from "./types";
 | 
			
		||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
 | 
			
		||||
 | 
			
		||||
@customElement("hui-glance-card")
 | 
			
		||||
export class HuiGlanceCard extends LitElement implements LovelaceCard {
 | 
			
		||||
@@ -86,7 +90,14 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
 | 
			
		||||
      state_color: true,
 | 
			
		||||
      ...config,
 | 
			
		||||
    };
 | 
			
		||||
    const entities = processConfigEntities<GlanceConfigEntity>(config.entities);
 | 
			
		||||
    const entities = processConfigEntities<GlanceConfigEntity>(
 | 
			
		||||
      config.entities
 | 
			
		||||
    ).map((entityConf) => {
 | 
			
		||||
      return {
 | 
			
		||||
        hold_action: { action: "more-info" } as MoreInfoActionConfig,
 | 
			
		||||
        ...entityConf,
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    for (const entity of entities) {
 | 
			
		||||
      if (
 | 
			
		||||
@@ -95,7 +106,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
 | 
			
		||||
          !entity.tap_action.service) ||
 | 
			
		||||
        (entity.hold_action &&
 | 
			
		||||
          entity.hold_action.action === "call-service" &&
 | 
			
		||||
          !entity.hold_action.service)
 | 
			
		||||
          !(entity.hold_action as CallServiceActionConfig).service)
 | 
			
		||||
      ) {
 | 
			
		||||
        throw new Error(
 | 
			
		||||
          'Missing required property "service" when tap_action or hold_action is call-service'
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user