diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a28fd83e4d..8b35690bb4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -18,8 +18,8 @@ ## Type of change @@ -56,7 +56,7 @@ --> - This PR fixes or closes issue: fixes # -- This PR is related to issue: +- This PR is related to issue or discussion: - Link to documentation pull request: ## Checklist diff --git a/.github/lock.yml b/.github/lock.yml deleted file mode 100644 index 2566272cb3..0000000000 --- a/.github/lock.yml +++ /dev/null @@ -1,27 +0,0 @@ -# Configuration for Lock Threads - https://github.com/dessant/lock-threads - -# Number of days of inactivity before a closed issue or pull request is locked -daysUntilLock: 1 - -# Skip issues and pull requests created before a given timestamp. Timestamp must -# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable -skipCreatedBefore: 2020-01-01 - -# Issues and pull requests with these labels will be ignored. Set to `[]` to disable -exemptLabels: [] - -# Label to add before locking, such as `outdated`. Set to `false` to disable -lockLabel: false - -# Comment to post before locking. Set to `false` to disable -lockComment: false - -# Assign `resolved` as the reason for locking. Set to `false` to disable -setLockReason: false - -# Limit to only `issues` or `pulls` -only: pulls - -# Optionally, specify configuration settings just for `issues` or `pulls` -issues: - daysUntilLock: 30 diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index dc0896c22c..0000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,56 +0,0 @@ -# Configuration for probot-stale - https://github.com/probot/stale - -# Number of days of inactivity before an Issue or Pull Request becomes stale -daysUntilStale: 90 - -# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. -# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. -daysUntilClose: 7 - -# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) -onlyLabels: [] - -# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable -exemptLabels: - - feature request - - Help wanted - - to do - -# Set to true to ignore issues in a project (defaults to false) -exemptProjects: true - -# Set to true to ignore issues in a milestone (defaults to false) -exemptMilestones: true - -# Set to true to ignore issues with an assignee (defaults to false) -exemptAssignees: false - -# Label to use when marking as stale -staleLabel: stale - -# Comment to post when marking as stale. Set to `false` to disable -markComment: > - There hasn't been any activity on this issue recently. Due to the high number - of incoming GitHub notifications, we have to clean some of the old issues, - as many of them have already been resolved with the latest updates. - - Please make sure to update to the latest Home Assistant version and check - if that solves the issue. Let us know if that works for you by adding a - comment 👍 - - This issue now has been marked as stale and will be closed if no further - activity occurs. Thank you for your contributions. - -# Comment to post when removing the stale label. -# unmarkComment: > -# Your comment here. - -# Comment to post when closing a stale Issue or Pull Request. -# closeComment: > -# Your comment here. - -# Limit the number of actions per hour, from 1-30. Default is 30 -limitPerRun: 30 - -# Limit to only `issues` or `pulls` -only: issues diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml new file mode 100644 index 0000000000..4f7a0efb2d --- /dev/null +++ b/.github/workflows/lock.yml @@ -0,0 +1,20 @@ +name: Lock + +# yamllint disable-line rule:truthy +on: + schedule: + - cron: "0 * * * *" + +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v2.0.1 + with: + github-token: ${{ github.token }} + issue-lock-inactive-days: "30" + issue-exclude-created-before: "2020-10-01T00:00:00Z" + issue-lock-reason: "" + pr-lock-inactive-days: "1" + pr-exclude-created-before: "2020-11-01T00:00:00Z" + pr-lock-reason: "" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..bcb543cf49 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,42 @@ +name: Stale + +# yamllint disable-line rule:truthy +on: + schedule: + - cron: "0 * * * *" + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - name: 90 days stale policy + uses: actions/stale@v3.0.13 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + days-before-stale: 90 + days-before-close: 7 + operations-per-run: 25 + remove-stale-when-updated: true + stale-issue-label: "stale" + exempt-issue-labels: "no-stale,Help%20wanted,help-wanted,feature-request,feature%20request" + stale-issue-message: > + There hasn't been any activity on this issue recently. Due to the + high number of incoming GitHub notifications, we have to clean some + of the old issues, as many of them have already been resolved with + the latest updates. + + Please make sure to update to the latest Home Assistant version and + check if that solves the issue. Let us know if that works for you by + adding a comment 👍 + + This issue has now been marked as stale and will be closed if no + further activity occurs. Thank you for your contributions. + + stale-pr-label: "stale" + exempt-pr-labels: "no-stale" + stale-pr-message: > + There hasn't been any activity on this pull request recently. This + pull request has been automatically marked as stale because of that + and will be closed if no further activity occurs within 7 days. + + Thank you for your contributions. diff --git a/build-scripts/gulp/translations.js b/build-scripts/gulp/translations.js index 19d329a7e0..e357d38edf 100755 --- a/build-scripts/gulp/translations.js +++ b/build-scripts/gulp/translations.js @@ -7,7 +7,6 @@ const gulp = require("gulp"); const fs = require("fs"); const foreach = require("gulp-foreach"); const merge = require("gulp-merge-json"); -const minify = require("gulp-jsonminify"); const rename = require("gulp-rename"); const transform = require("gulp-json-transform"); const { mapFiles } = require("../util"); @@ -301,7 +300,6 @@ gulp.task("build-flattened-translations", function () { return flatten(data); }) ) - .pipe(minify()) .pipe( rename((filePath) => { if (filePath.dirname === "core") { diff --git a/demo/src/configs/teachingbirds/lovelace.ts b/demo/src/configs/teachingbirds/lovelace.ts index f7684fde6d..135dde6a0b 100644 --- a/demo/src/configs/teachingbirds/lovelace.ts +++ b/demo/src/configs/teachingbirds/lovelace.ts @@ -7,205 +7,183 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({ cards: [ { type: "custom:ha-demo-card" }, { + type: "grid", + columns: 4, cards: [ { - cards: [ + image: "/assets/teachingbirds/isa_square.jpg", + type: "picture-entity", + show_name: false, + tap_action: { + action: "more-info", + }, + entity: "sensor.presence_isa", + }, + { + image: "/assets/teachingbirds/Stefan_square.jpg", + type: "picture-entity", + show_name: false, + tap_action: { + action: "more-info", + }, + entity: "sensor.presence_stefan", + }, + { + image: "/assets/teachingbirds/background_square.png", + elements: [ { - image: "/assets/teachingbirds/isa_square.jpg", - type: "picture-entity", - show_name: false, + state_image: { + on: "/assets/teachingbirds/radiator_on.jpg", + off: "/assets/teachingbirds/radiator_off.jpg", + }, + type: "image", + style: { + width: "100%", + top: "50%", + left: "50%", + }, tap_action: { action: "more-info", }, - entity: "sensor.presence_isa", + entity: "switch.stefan_radiator_3", }, { - image: "/assets/teachingbirds/Stefan_square.jpg", - type: "picture-entity", - show_name: false, - tap_action: { - action: "more-info", + style: { + top: "90%", + left: "50%", }, - entity: "sensor.presence_stefan", - }, - { - image: "/assets/teachingbirds/background_square.png", - elements: [ - { - state_image: { - on: "/assets/teachingbirds/radiator_on.jpg", - off: "/assets/teachingbirds/radiator_off.jpg", - }, - type: "image", - style: { - width: "100%", - top: "50%", - left: "50%", - }, - tap_action: { - action: "more-info", - }, - entity: "switch.stefan_radiator_3", - }, - { - style: { - top: "90%", - left: "50%", - }, - type: "state-label", - entity: "sensor.temperature_stefan", - }, - ], - type: "picture-elements", - }, - { - image: "/assets/teachingbirds/background_square.png", - elements: [ - { - style: { - "--mdc-icon-size": "100%", - top: "50%", - left: "50%", - }, - type: "icon", - tap_action: { - action: "navigate", - navigation_path: "/lovelace/home_info", - }, - icon: "mdi:car", - }, - ], - type: "picture-elements", - }, - ], - type: "horizontal-stack", - }, - { - cards: [ - { - show_name: false, - type: "picture-entity", - name: "Alarm", - image: "/assets/teachingbirds/House_square.jpg", - entity: "alarm_control_panel.house", - }, - { - name: "Roomba", - image: "/assets/teachingbirds/roomba_square.jpg", - show_name: false, - type: "picture-entity", - state_image: { - "Not Today": "/assets/teachingbirds/roomba_bw_square.jpg", - }, - entity: "input_select.roomba_mode", - }, - { - show_name: false, - type: "picture-entity", - state_image: { - Mail: "/assets/teachingbirds/mailbox_square.jpg", - "Package and mail": - "/assets/teachingbirds/mailbox_square.jpg", - Empty: "/assets/teachingbirds/mailbox_bw_square.jpg", - Package: "/assets/teachingbirds/mailbox_square.jpg", - }, - entity: "sensor.mailbox", - }, - { - show_name: false, - state_image: { - "Put out": "/assets/teachingbirds/trash_square.jpg", - "Take in": "/assets/teachingbirds/trash_square.jpg", - }, - type: "picture-entity", - image: "/assets/teachingbirds/trash_bear_bw_square.jpg", - entity: "sensor.trash_status", - }, - ], - type: "horizontal-stack", - }, - { - cards: [ - { - state_image: { - Idle: "/assets/teachingbirds/washer_square.jpg", - Running: "/assets/teachingbirds/laundry_running_square.jpg", - Clean: "/assets/teachingbirds/laundry_clean_2_square.jpg", - }, - entity: "input_select.washing_machine_status", - type: "picture-entity", - show_name: false, - name: "Washer", - }, - { - state_image: { - Idle: "/assets/teachingbirds/dryer_square.jpg", - Running: "/assets/teachingbirds/clothes_drying_square.jpg", - Clean: "/assets/teachingbirds/folded_clothes_square.jpg", - }, - entity: "input_select.dryer_status", - type: "picture-entity", - show_name: false, - name: "Dryer", - }, - { - image: "/assets/teachingbirds/guests_square.jpg", - type: "picture-entity", - show_name: false, - tap_action: { - action: "toggle", - }, - entity: "input_boolean.guest_mode", - }, - { - image: "/assets/teachingbirds/cleaning_square.jpg", - type: "picture-entity", - show_name: false, - tap_action: { - action: "toggle", - }, - entity: "input_boolean.cleaning_day", - }, - ], - type: "horizontal-stack", - }, - ], - type: "vertical-stack", - }, - { - type: "vertical-stack", - cards: [ - { - cards: [ - { - graph: "line", - type: "sensor", - entity: "sensor.temperature_bedroom", - }, - { - graph: "line", - type: "sensor", - name: "S's room", + type: "state-label", entity: "sensor.temperature_stefan", }, ], - type: "horizontal-stack", + type: "picture-elements", }, { - cards: [ + image: "/assets/teachingbirds/background_square.png", + elements: [ { - graph: "line", - type: "sensor", - entity: "sensor.temperature_passage", - }, - { - graph: "line", - type: "sensor", - name: "Laundry", - entity: "sensor.temperature_downstairs_bathroom", + style: { + "--mdc-icon-size": "100%", + top: "50%", + left: "50%", + }, + type: "icon", + tap_action: { + action: "navigate", + navigation_path: "/lovelace/home_info", + }, + icon: "mdi:car", }, ], - type: "horizontal-stack", + type: "picture-elements", + }, + + { + show_name: false, + type: "picture-entity", + name: "Alarm", + image: "/assets/teachingbirds/House_square.jpg", + entity: "alarm_control_panel.house", + }, + { + name: "Roomba", + image: "/assets/teachingbirds/roomba_square.jpg", + show_name: false, + type: "picture-entity", + state_image: { + "Not Today": "/assets/teachingbirds/roomba_bw_square.jpg", + }, + entity: "input_select.roomba_mode", + }, + { + show_name: false, + type: "picture-entity", + state_image: { + Mail: "/assets/teachingbirds/mailbox_square.jpg", + "Package and mail": "/assets/teachingbirds/mailbox_square.jpg", + Empty: "/assets/teachingbirds/mailbox_bw_square.jpg", + Package: "/assets/teachingbirds/mailbox_square.jpg", + }, + entity: "sensor.mailbox", + }, + { + show_name: false, + state_image: { + "Put out": "/assets/teachingbirds/trash_square.jpg", + "Take in": "/assets/teachingbirds/trash_square.jpg", + }, + type: "picture-entity", + image: "/assets/teachingbirds/trash_bear_bw_square.jpg", + entity: "sensor.trash_status", + }, + + { + state_image: { + Idle: "/assets/teachingbirds/washer_square.jpg", + Running: "/assets/teachingbirds/laundry_running_square.jpg", + Clean: "/assets/teachingbirds/laundry_clean_2_square.jpg", + }, + entity: "input_select.washing_machine_status", + type: "picture-entity", + show_name: false, + name: "Washer", + }, + { + state_image: { + Idle: "/assets/teachingbirds/dryer_square.jpg", + Running: "/assets/teachingbirds/clothes_drying_square.jpg", + Clean: "/assets/teachingbirds/folded_clothes_square.jpg", + }, + entity: "input_select.dryer_status", + type: "picture-entity", + show_name: false, + name: "Dryer", + }, + { + image: "/assets/teachingbirds/guests_square.jpg", + type: "picture-entity", + show_name: false, + tap_action: { + action: "toggle", + }, + entity: "input_boolean.guest_mode", + }, + { + image: "/assets/teachingbirds/cleaning_square.jpg", + type: "picture-entity", + show_name: false, + tap_action: { + action: "toggle", + }, + entity: "input_boolean.cleaning_day", + }, + ], + }, + { + type: "grid", + columns: 2, + cards: [ + { + graph: "line", + type: "sensor", + entity: "sensor.temperature_bedroom", + }, + { + graph: "line", + type: "sensor", + name: "S's room", + entity: "sensor.temperature_stefan", + }, + { + graph: "line", + type: "sensor", + entity: "sensor.temperature_passage", + }, + { + graph: "line", + type: "sensor", + name: "Laundry", + entity: "sensor.temperature_downstairs_bathroom", }, ], }, diff --git a/demo/src/stubs/template.ts b/demo/src/stubs/template.ts index 726926461c..0e3c1a2638 100644 --- a/demo/src/stubs/template.ts +++ b/demo/src/stubs/template.ts @@ -6,4 +6,11 @@ export const mockTemplate = (hass: MockHomeAssistant) => { body: { message: "Template dev tool does not work in the demo." }, }) ); + hass.mockWS("render_template", (msg, onChange) => { + onChange!({ + result: msg.template, + listeners: { all: false, domains: [], entities: [], time: false }, + }); + return () => {}; + }); }; diff --git a/gallery/public/images/sunflowers.jpg b/gallery/public/images/sunflowers.jpg new file mode 100644 index 0000000000..961d2a7bf3 Binary files /dev/null and b/gallery/public/images/sunflowers.jpg differ diff --git a/gallery/src/components/demo-cards.js b/gallery/src/components/demo-cards.js index 26ac82e3f7..9e076371c4 100644 --- a/gallery/src/components/demo-cards.js +++ b/gallery/src/components/demo-cards.js @@ -5,11 +5,16 @@ import { PolymerElement } from "@polymer/polymer/polymer-element"; import "../../../src/components/ha-switch"; import "../../../src/components/ha-formfield"; import "./demo-card"; +import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element"; class DemoCards extends PolymerElement { static get template() { return html`
@@ -31,16 +39,21 @@ class DemoCards extends PolymerElement { + + +
-
- +
+
+ +
`; } @@ -59,6 +72,12 @@ class DemoCards extends PolymerElement { _showConfigToggled(ev) { this._showConfig = ev.target.checked; } + + _darkThemeToggled(ev) { + applyThemesOnElement(this.$.container, { themes: {} }, "default", { + dark: ev.target.checked, + }); + } } customElements.define("demo-cards", DemoCards); diff --git a/gallery/src/components/demo-more-info.js b/gallery/src/components/demo-more-info.js index 9e4a34023f..0805e77c37 100644 --- a/gallery/src/components/demo-more-info.js +++ b/gallery/src/components/demo-more-info.js @@ -3,7 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import "../../../src/components/ha-card"; import "../../../src/state-summary/state-card-content"; -import "./more-info-content"; +import "../../../src/dialogs/more-info/more-info-content"; class DemoMoreInfo extends PolymerElement { static get template() { @@ -16,15 +16,12 @@ class DemoMoreInfo extends PolymerElement { ha-card { width: 333px; + padding: 20px 24px; } state-card-content { display: block; - padding: 16px; - } - - more-info-content { - padding: 0 16px; + margin-bottom: 16px; } pre { diff --git a/gallery/src/components/more-info-content.ts b/gallery/src/components/more-info-content.ts deleted file mode 100644 index 549ac4fa2d..0000000000 --- a/gallery/src/components/more-info-content.ts +++ /dev/null @@ -1,73 +0,0 @@ -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/dialogs/more-info/state_more_info_control"; -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); diff --git a/gallery/src/data/media_players.ts b/gallery/src/data/media_players.ts index a28f679c53..07dc174e8c 100644 --- a/gallery/src/data/media_players.ts +++ b/gallery/src/data/media_players.ts @@ -6,7 +6,9 @@ export const createMediaPlayerEntities = () => [ media_content_type: "music", media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", media_artist: "Technohead", - supported_features: 64063, + // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + + // Select Source + Stop + Clear + Play + Shuffle Set + Browse Media + supported_features: 195135, entity_picture: "/images/album_cover_2.jpg", media_duration: 300, media_position: 50, @@ -14,12 +16,15 @@ export const createMediaPlayerEntities = () => [ // 23 seconds in new Date().getTime() - 23000 ).toISOString(), + volume_level: 0.5, }), getEntity("media_player", "music_playing", "playing", { friendly_name: "Playing The Music", media_content_type: "music", media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", media_artist: "Technohead", + // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + + // Select Source + Stop + Clear + Play + Shuffle Set supported_features: 64063, entity_picture: "/images/album_cover.jpg", media_duration: 300, @@ -28,6 +33,7 @@ export const createMediaPlayerEntities = () => [ // 23 seconds in new Date().getTime() - 23000 ).toISOString(), + volume_level: 0.5, }), getEntity("media_player", "stream_playing", "playing", { friendly_name: "Playing the Stream", @@ -35,50 +41,125 @@ export const createMediaPlayerEntities = () => [ media_title: "Epic sax guy 10 hours", app_name: "YouTube", entity_picture: "/images/frenck.jpg", - supported_features: 33, + // Pause + Next Track + Play + Browse Media + supported_features: 147489, }), - getEntity("media_player", "living_room", "playing", { - friendly_name: "Pause, No skip, tvshow", + getEntity("media_player", "stream_paused", "paused", { + friendly_name: "Paused the Stream", + media_content_type: "movie", + media_title: "Epic sax guy 10 hours", + app_name: "YouTube", + entity_picture: "/images/frenck.jpg", + // Pause + Next Track + Play + supported_features: 16417, + }), + getEntity("media_player", "stream_playing_previous", "playing", { + friendly_name: 'Playing the Stream (with "previous" support)', + media_content_type: "movie", + media_title: "Epic sax guy 10 hours", + app_name: "YouTube", + entity_picture: "/images/frenck.jpg", + // Pause + Previous Track + Play + supported_features: 16401, + }), + getEntity("media_player", "tv_playing", "playing", { + friendly_name: "Playing non-skip TV Show", media_content_type: "tvshow", media_title: "Chapter 1", media_series_title: "House of Cards", app_name: "Netflix", entity_picture: "/images/netflix.jpg", + // Pause supported_features: 1, }), getEntity("media_player", "sonos_idle", "idle", { friendly_name: "Sonos Idle", + // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + + // Select Source + Stop + Clear + Play + Shuffle Set supported_features: 64063, + volume_level: 0.33, + is_volume_muted: true, }), - getEntity("media_player", "theater", "off", { + getEntity("media_player", "idle_browse_media", "idle", { + friendly_name: "Idle waiting for Browse Media (e.g. Spotify)", + // Pause + Seek + Volume Set + Previous Track + Next Track + Play Media + + // Select Source + Play + Shuffle Set + Browse Media + supported_features: 182839, + volume_level: 0.79, + }), + getEntity("media_player", "theater_off", "off", { friendly_name: "TV Off", + // On + Off + Play + Next + Pause + supported_features: 16801, + }), + getEntity("media_player", "theater_on", "on", { + friendly_name: "TV On", + // On + Off + Play + Next + Pause + supported_features: 16801, + }), + getEntity("media_player", "theater_off_static", "off", { + friendly_name: "TV Off (cannot be switched on)", + // Off + Next + Pause + supported_features: 289, + }), + getEntity("media_player", "theater_on_static", "on", { + friendly_name: "TV On (cannot be switched off)", + // On + Next + Pause supported_features: 161, }), getEntity("media_player", "android_cast", "playing", { - friendly_name: "Casting App", + friendly_name: "Casting App (no supported features)", media_title: "Android Screen Casting", app_name: "Screen Mirroring", - // supported_features: 21437, + }), + getEntity("media_player", "image_display", "playing", { + friendly_name: "Digital Picture Frame", + media_content_type: "image", + media_title: "Famous Painting", + media_artist: "Famous Artist", + entity_picture: "/images/sunflowers.jpg", + // On + Off + Browse Media + supported_features: 131456, }), getEntity("media_player", "unavailable", "unavailable", { friendly_name: "Player Unavailable", + // Pause + Volume Set + Volume Mute + Previous Track + Next Track + + // Play Media + Stop + Play supported_features: 21437, }), getEntity("media_player", "unknown", "unknown", { friendly_name: "Player Unknown", + // Pause + Volume Set + Volume Mute + Previous Track + Next Track + + // Play Media + Stop + Play supported_features: 21437, }), + getEntity("media_player", "playing", "playing", { + friendly_name: "Player Playing (no Pause support)", + // Volume Set + Volume Mute + Previous Track + Next Track + + // Play Media + Stop + Play + supported_features: 21436, + volume_level: 1, + }), + getEntity("media_player", "idle", "idle", { + friendly_name: "Player Idle", + // Pause + Volume Set + Volume Mute + Previous Track + Next Track + + // Play Media + Stop + Play + supported_features: 21437, + volume_level: 0, + }), getEntity("media_player", "receiver_on", "on", { source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"], volume_level: 0.63, is_volume_muted: false, source: "TV", - friendly_name: "Receiver", + friendly_name: "Receiver (selectable sources)", + // Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode supported_features: 84364, }), getEntity("media_player", "receiver_off", "off", { source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"], - friendly_name: "Receiver", + friendly_name: "Receiver (selectable sources)", + // Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode supported_features: 84364, }), ]; diff --git a/gallery/src/demos/demo-hui-alarm-panel-card.ts b/gallery/src/demos/demo-hui-alarm-panel-card.ts index 12b5f130b4..73d46b53fd 100644 --- a/gallery/src/demos/demo-hui-alarm-panel-card.ts +++ b/gallery/src/demos/demo-hui-alarm-panel-card.ts @@ -15,6 +15,10 @@ const ENTITIES = [ getEntity("alarm_control_panel", "unavailable", "unavailable", { friendly_name: "Alarm", }), + getEntity("alarm_control_panel", "alarm_code", "disarmed", { + friendly_name: "Alarm", + code_format: "number", + }), ]; const CONFIGS = [ @@ -30,7 +34,14 @@ const CONFIGS = [ config: ` - type: alarm-panel entity: alarm_control_panel.alarm_armed - title: My Alarm + name: My Alarm + `, + }, + { + heading: "Code Example", + config: ` +- type: alarm-panel + entity: alarm_control_panel.alarm_code `, }, { @@ -83,8 +94,12 @@ class DemoAlarmPanelEntity extends PolymerElement { public ready() { super.ready(); + this._setupDemo(); + } + + private async _setupDemo() { const hass = provideHass(this.$.demos); - hass.updateTranslations(null, "en"); + await hass.updateTranslations(null, "en"); hass.addEntities(ENTITIES); } } diff --git a/gallery/src/demos/demo-hui-entity-button-card.ts b/gallery/src/demos/demo-hui-entity-button-card.ts index 8c4c77e100..5b89327432 100644 --- a/gallery/src/demos/demo-hui-entity-button-card.ts +++ b/gallery/src/demos/demo-hui-entity-button-card.ts @@ -98,4 +98,4 @@ class DemoButtonEntity extends PolymerElement { } } -customElements.define("demo-hui-button-card", DemoButtonEntity); +customElements.define("demo-hui-entity-button-card", DemoButtonEntity); diff --git a/gallery/src/demos/demo-hui-gauge-card.ts b/gallery/src/demos/demo-hui-gauge-card.ts index 2963cab228..5dd0cf582b 100644 --- a/gallery/src/demos/demo-hui-gauge-card.ts +++ b/gallery/src/demos/demo-hui-gauge-card.ts @@ -8,6 +8,7 @@ import "../components/demo-cards"; const ENTITIES = [ getEntity("sensor", "brightness", "12", {}), getEntity("plant", "bonsai", "ok", {}), + getEntity("sensor", "not_working", "unavailable", {}), getEntity("sensor", "outside_humidity", "54", { unit_of_measurement: "%", }), @@ -74,6 +75,13 @@ const CONFIGS = [ entity: plant.bonsai `, }, + { + heading: "Unavailable entity", + config: ` +- type: gauge + entity: sensor.not_working + `, + }, ]; class DemoGaugeEntity extends PolymerElement { diff --git a/gallery/src/demos/demo-hui-markdown-card.ts b/gallery/src/demos/demo-hui-markdown-card.ts index 07e974316e..1ae6b5f0f7 100644 --- a/gallery/src/demos/demo-hui-markdown-card.ts +++ b/gallery/src/demos/demo-hui-markdown-card.ts @@ -1,6 +1,8 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; /* eslint-plugin-disable lit */ import { PolymerElement } from "@polymer/polymer/polymer-element"; +import { mockTemplate } from "../../../demo/src/stubs/template"; +import { provideHass } from "../../../src/fake_data/provide_hass"; import "../components/demo-cards"; const CONFIGS = [ @@ -254,7 +256,7 @@ const CONFIGS = [ class DemoMarkdown extends PolymerElement { static get template() { - return html` `; + return html` `; } static get properties() { @@ -265,6 +267,12 @@ class DemoMarkdown extends PolymerElement { }, }; } + + public ready() { + super.ready(); + const hass = provideHass(this.$.demos); + mockTemplate(hass); + } } customElements.define("demo-hui-markdown-card", DemoMarkdown); diff --git a/gallery/src/demos/demo-hui-media-control-card.ts b/gallery/src/demos/demo-hui-media-control-card.ts index a26a13a8a3..6298163a25 100644 --- a/gallery/src/demos/demo-hui-media-control-card.ts +++ b/gallery/src/demos/demo-hui-media-control-card.ts @@ -7,40 +7,61 @@ import { createMediaPlayerEntities } from "../data/media_players"; const CONFIGS = [ { - heading: "Paused music", + heading: "Paused Music", config: ` - type: media-control entity: media_player.music_paused `, }, { - heading: "Playing music", + heading: "Playing Music", config: ` - type: media-control entity: media_player.music_playing `, }, { - heading: "Playing stream", + heading: "Playing Stream", config: ` - type: media-control entity: media_player.stream_playing `, }, { - heading: "Pause, No skip, tvshow", + heading: "Paused Stream", config: ` - type: media-control - entity: media_player.living_room + entity: media_player.stream_paused `, }, { - heading: "Screen casting", + heading: 'Playing Stream (with "previous" support)', + config: ` + - type: media-control + entity: media_player.stream_playing_previous + `, + }, + { + heading: "Playing non-skip TV Show", + config: ` + - type: media-control + entity: media_player.tv_playing + `, + }, + { + heading: "Screen Casting", config: ` - type: media-control entity: media_player.android_cast `, }, + { + heading: "Digital Picture Frame", + config: ` + - type: media-control + entity: media_player.image_display + `, + }, { heading: "Sonos Idle", config: ` @@ -48,11 +69,53 @@ const CONFIGS = [ entity: media_player.sonos_idle `, }, + { + heading: "Idle waiting for Browse Media", + config: ` + - type: media-control + entity: media_player.idle_browse_media + `, + }, { heading: "Player Off", config: ` - type: media-control - entity: media_player.theater + entity: media_player.theater_off + `, + }, + { + heading: "Player On", + config: ` + - type: media-control + entity: media_player.theater_on + `, + }, + { + heading: "Player Off (cannot be switched on)", + config: ` + - type: media-control + entity: media_player.theater_off_static + `, + }, + { + heading: "Player On (cannot be switched off)", + config: ` + - type: media-control + entity: media_player.theater_on_static + `, + }, + { + heading: "Player Idle", + config: ` + - type: media-control + entity: media_player.idle + `, + }, + { + heading: "Player Playing", + config: ` + - type: media-control + entity: media_player.playing `, }, { @@ -70,14 +133,14 @@ const CONFIGS = [ `, }, { - heading: "Receiver On", + heading: "Receiver On (selectable sources)", config: ` - type: media-control entity: media_player.receiver_on `, }, { - heading: "Receiver Off", + heading: "Receiver Off (selectable sources)", config: ` - type: media-control entity: media_player.receiver_off diff --git a/gallery/src/demos/demo-hui-media-player-rows.ts b/gallery/src/demos/demo-hui-media-player-rows.ts index 1077e7a7b8..8287f5b773 100644 --- a/gallery/src/demos/demo-hui-media-player-rows.ts +++ b/gallery/src/demos/demo-hui-media-player-rows.ts @@ -12,23 +12,45 @@ const CONFIGS = [ - type: entities entities: - entity: media_player.music_paused - name: Paused music + name: Paused Music - entity: media_player.music_playing - name: Playing music + name: Playing Music - entity: media_player.stream_playing - name: Paused, no play - - entity: media_player.living_room - name: Pause, No skip, tvshow + name: Playing Stream + - entity: media_player.stream_paused + name: Paused Stream + - entity: media_player.stream_playing_previous + name: Playing Stream (with "previous" support) + - entity: media_player.tv_playing + name: Playing non-skip TV Show - entity: media_player.android_cast name: Screen casting + - entity: media_player.image_display + name: Digital Picture Frame - entity: media_player.sonos_idle - name: Chromcast Idle - - entity: media_player.theater + name: Sonos Idle + - entity: media_player.idle_browse_media + name: Idle waiting for Browse Media + - entity: media_player.theater_off name: Player Off + - entity: media_player.theater_on + name: Player On + - entity: media_player.theater_off_static + name: Player Off (cannot be switched on) + - entity: media_player.theater_on_static + name: Player On (cannot be switched off) + - entity: media_player.idle + name: Player Idle + - entity: media_player.playing + name: Player Playing - entity: media_player.unavailable name: Player Unavailable - entity: media_player.unknown name: Player Unknown + - entity: media_player.receiver_on + name: Receiver On (selectable sources) + - entity: media_player.receiver_off + name: Receiver Off (selectable sources) `, }, ]; diff --git a/gallery/src/demos/demo-hui-stack-card.ts b/gallery/src/demos/demo-hui-stack-card.ts index b746888dc1..982eab4f25 100644 --- a/gallery/src/demos/demo-hui-stack-card.ts +++ b/gallery/src/demos/demo-hui-stack-card.ts @@ -1,6 +1,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; /* eslint-plugin-disable lit */ import { PolymerElement } from "@polymer/polymer/polymer-element"; +import { mockHistory } from "../../../demo/src/stubs/history"; import { getEntity } from "../../../src/fake_data/entity"; import { provideHass } from "../../../src/fake_data/provide_hass"; import "../components/demo-cards"; @@ -36,6 +37,10 @@ const ENTITIES = [ battery: 71, friendly_name: "Home Boy", }), + getEntity("sensor", "illumination", "23", { + friendly_name: "Illumination", + unit_of_measurement: "lx", + }), ]; const CONFIGS = [ @@ -89,6 +94,42 @@ const CONFIGS = [ entity: light.bed_light `, }, + { + heading: "Default Grid", + config: ` +- type: grid + cards: + - type: entity + entity: light.kitchen_lights + - type: entity + entity: light.bed_light + - type: entity + entity: device_tracker.demo_paulus + - type: sensor + entity: sensor.illumination + graph: line + - type: entity + entity: device_tracker.demo_anne_therese + `, + }, + { + heading: "Non-square Grid with 2 columns", + config: ` +- type: grid + columns: 2 + square: false + cards: + - type: entity + entity: light.kitchen_lights + - type: entity + entity: light.bed_light + - type: entity + entity: device_tracker.demo_paulus + - type: sensor + entity: sensor.illumination + graph: line + `, + }, ]; class DemoStack extends PolymerElement { @@ -110,6 +151,7 @@ class DemoStack extends PolymerElement { const hass = provideHass(this.$.demos); hass.updateTranslations(null, "en"); hass.addEntities(ENTITIES); + mockHistory(hass); } } diff --git a/gallery/src/demos/demo-more-info-light.ts b/gallery/src/demos/demo-more-info-light.ts index 8c5ac611e9..70b77560d9 100644 --- a/gallery/src/demos/demo-more-info-light.ts +++ b/gallery/src/demos/demo-more-info-light.ts @@ -6,7 +6,7 @@ import { SUPPORT_BRIGHTNESS } from "../../../src/data/light"; 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"; +import "../../../src/dialogs/more-info/more-info-content"; const ENTITIES = [ getEntity("light", "bed_light", "on", { @@ -40,8 +40,12 @@ class DemoMoreInfoLight extends PolymerElement { public ready() { super.ready(); + this._setupDemo(); + } + + private async _setupDemo() { const hass = provideHass(this); - hass.updateTranslations(null, "en"); + await hass.updateTranslations(null, "en"); hass.addEntities(ENTITIES); } } diff --git a/hassio/src/ingress-view/hassio-ingress-view.ts b/hassio/src/ingress-view/hassio-ingress-view.ts index ec899c7094..14962c6f36 100644 --- a/hassio/src/ingress-view/hassio-ingress-view.ts +++ b/hassio/src/ingress-view/hassio-ingress-view.ts @@ -13,7 +13,10 @@ import { fetchHassioAddonInfo, HassioAddonDetails, } from "../../../src/data/hassio/addon"; -import { createHassioSession } from "../../../src/data/hassio/supervisor"; +import { + createHassioSession, + validateHassioSession, +} from "../../../src/data/hassio/ingress"; import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-subpage"; import { HomeAssistant, Route } from "../../../src/types"; @@ -35,6 +38,17 @@ class HassioIngressView extends LitElement { @property({ type: Boolean }) public narrow = false; + private _sessionKeepAlive?: number; + + public disconnectedCallback() { + super.disconnectedCallback(); + + if (this._sessionKeepAlive) { + clearInterval(this._sessionKeepAlive); + this._sessionKeepAlive = undefined; + } + } + protected render(): TemplateResult { if (!this._addon) { return html` `; @@ -44,6 +58,7 @@ class HassioIngressView extends LitElement { if (!this.ingressPanel) { return html` @@ -83,10 +98,7 @@ class HassioIngressView extends LitElement { } private async _fetchData(addonSlug: string) { - const createSessionPromise = createHassioSession(this.hass).then( - () => true, - () => false - ); + const createSessionPromise = createHassioSession(this.hass); let addon; @@ -119,7 +131,11 @@ class HassioIngressView extends LitElement { return; } - if (!(await createSessionPromise)) { + let session; + + try { + session = await createSessionPromise; + } catch (err) { await showAlertDialog(this, { text: "Unable to create an Ingress session", title: addon.name, @@ -128,6 +144,17 @@ class HassioIngressView extends LitElement { return; } + if (this._sessionKeepAlive) { + clearInterval(this._sessionKeepAlive); + } + this._sessionKeepAlive = window.setInterval(async () => { + try { + await validateHassioSession(this.hass, session); + } catch (err) { + session = await createHassioSession(this.hass); + } + }, 60000); + this._addon = addon; } diff --git a/package.json b/package.json index 483b6ea952..7fc0f96c56 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,6 @@ "gulp": "^4.0.0", "gulp-foreach": "^0.1.0", "gulp-json-transform": "^0.4.6", - "gulp-jsonminify": "^1.1.0", "gulp-merge-json": "^1.3.1", "gulp-rename": "^2.0.0", "gulp-zopfli-green": "^3.0.1", diff --git a/polymer.json b/polymer.json index 47df9b9c7d..430067d045 100644 --- a/polymer.json +++ b/polymer.json @@ -13,7 +13,6 @@ "src/panels/iframe/ha-panel-iframe.js", "src/panels/logbook/ha-panel-logbook.js", "src/panels/map/ha-panel-map.js", - "src/panels/shopping-list/ha-panel-shopping-list.js", "src/panels/mailbox/ha-panel-mailbox.js", "hassio/src/entrypoint.js" ], diff --git a/public/static/images/conference.png b/public/static/images/conference.png new file mode 100644 index 0000000000..591028a8b7 Binary files /dev/null and b/public/static/images/conference.png differ diff --git a/setup.py b/setup.py index e6b913852e..018b54ffc9 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20201021.4", + version="20201111.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/common/color/convert-color.ts b/src/common/color/convert-color.ts index f48e6184a0..29f95b4283 100644 --- a/src/common/color/convert-color.ts +++ b/src/common/color/convert-color.ts @@ -1,10 +1,4 @@ -const expand_hex = (hex: string): string => { - let result = ""; - for (const val of hex) { - result += val + val; - } - return result; -}; +import { expandHex } from "./hex"; const rgb_hex = (component: number): string => { const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16); @@ -14,10 +8,7 @@ const rgb_hex = (component: number): string => { // Conversion between HEX and RGB export const hex2rgb = (hex: string): [number, number, number] => { - hex = hex.replace("#", ""); - if (hex.length === 3 || hex.length === 4) { - hex = expand_hex(hex); - } + hex = expandHex(hex); return [ parseInt(hex.substring(0, 2), 16), diff --git a/src/common/color/hex.ts b/src/common/color/hex.ts new file mode 100644 index 0000000000..a25b6d6442 --- /dev/null +++ b/src/common/color/hex.ts @@ -0,0 +1,24 @@ +export const expandHex = (hex: string): string => { + hex = hex.replace("#", ""); + if (hex.length === 6) return hex; + let result = ""; + for (const val of hex) { + result += val + val; + } + return result; +}; + +// Blend 2 hex colors: c1 is placed over c2, blend is c1's opacity. +export const hexBlend = (c1: string, c2: string, blend = 50): string => { + let color = ""; + c1 = expandHex(c1); + c2 = expandHex(c2); + for (let i = 0; i <= 5; i += 2) { + const h1 = parseInt(c1.substr(i, 2), 16); + const h2 = parseInt(c2.substr(i, 2), 16); + let hex = Math.floor(h2 + (h1 - h2) * (blend / 100)).toString(16); + while (hex.length < 2) hex = "0" + hex; + color += hex; + } + return `#${color}`; +}; diff --git a/src/common/config/can_show_page.ts b/src/common/config/can_show_page.ts new file mode 100644 index 0000000000..0ac4f6d2ec --- /dev/null +++ b/src/common/config/can_show_page.ts @@ -0,0 +1,18 @@ +import { isComponentLoaded } from "./is_component_loaded"; +import { PageNavigation } from "../../layouts/hass-tabs-subpage"; +import { HomeAssistant } from "../../types"; + +export const canShowPage = (hass: HomeAssistant, page: PageNavigation) => { + return ( + (isCore(page) || isLoadedIntegration(hass, page)) && + !hideAdvancedPage(hass, page) + ); +}; + +const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) => + !page.component || isComponentLoaded(hass, page.component); +const isCore = (page: PageNavigation) => page.core; +const isAdvancedPage = (page: PageNavigation) => page.advancedOnly; +const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced; +const hideAdvancedPage = (hass: HomeAssistant, page: PageNavigation) => + isAdvancedPage(page) && !userWantsAdvanced(hass); diff --git a/src/common/dom/apply_themes_on_element.ts b/src/common/dom/apply_themes_on_element.ts index 902a1a196a..4e1aeb9ec6 100644 --- a/src/common/dom/apply_themes_on_element.ts +++ b/src/common/dom/apply_themes_on_element.ts @@ -7,6 +7,7 @@ import { rgb2hex, rgb2lab, } from "../color/convert-color"; +import { hexBlend } from "../color/hex"; import { labBrighten, labDarken } from "../color/lab"; import { rgbContrast } from "../color/rgb"; @@ -37,6 +38,13 @@ export const applyThemesOnElement = ( if (themeOptions.dark) { cacheKey = `${cacheKey}__dark`; themeRules = darkStyles; + if (themeOptions.primaryColor) { + themeRules["app-header-background-color"] = hexBlend( + themeOptions.primaryColor, + "#121212", + 8 + ); + } } if (themeOptions.primaryColor) { cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`; diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 2a437901ea..d588cd1688 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -5,6 +5,7 @@ import { formatDateTime } from "../datetime/format_date_time"; import { formatTime } from "../datetime/format_time"; import { LocalizeFunc } from "../translations/localize"; import { computeStateDomain } from "./compute_state_domain"; +import { numberFormat } from "../string/number-format"; export const computeStateDisplay = ( localize: LocalizeFunc, @@ -19,7 +20,9 @@ export const computeStateDisplay = ( } if (stateObj.attributes.unit_of_measurement) { - return `${compareState} ${stateObj.attributes.unit_of_measurement}`; + return `${numberFormat(compareState, language)} ${ + stateObj.attributes.unit_of_measurement + }`; } const domain = computeStateDomain(stateObj); diff --git a/src/common/entity/cover_icon.ts b/src/common/entity/cover_icon.ts index 5723ee2d34..7c9bc5ed53 100644 --- a/src/common/entity/cover_icon.ts +++ b/src/common/entity/cover_icon.ts @@ -43,6 +43,7 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => { } case "blind": case "curtain": + case "shade": switch (state) { case "opening": return "hass:arrow-up-box"; diff --git a/src/common/string/number-format.ts b/src/common/string/number-format.ts new file mode 100644 index 0000000000..39d20e47a5 --- /dev/null +++ b/src/common/string/number-format.ts @@ -0,0 +1,22 @@ +/** + * Formats a number based on the specified language with thousands separator(s) and decimal character for better legibility. + * + * @param num The number to format + * @param language The language to use when formatting the number + */ +export const numberFormat = ( + num: string | number, + language: string +): string => { + // Polyfill for Number.isNaN, which is more reliable that the global isNaN() + Number.isNaN = + Number.isNaN || + function isNaN(input) { + return typeof input === "number" && isNaN(input); + }; + + if (!Number.isNaN(Number(num)) && Intl) { + return new Intl.NumberFormat(language).format(Number(num)); + } + return num.toString(); +}; diff --git a/src/common/translations/localize.ts b/src/common/translations/localize.ts index fa6adc1b52..6ebba251b8 100644 --- a/src/common/translations/localize.ts +++ b/src/common/translations/localize.ts @@ -15,10 +15,10 @@ export interface FormatsType { let polyfillLoaded = !shouldPolyfill(); const polyfillProm = polyfillLoaded - ? import("@formatjs/intl-pluralrules/polyfill-locales").then(() => { + ? undefined + : import("@formatjs/intl-pluralrules/polyfill-locales").then(() => { polyfillLoaded = true; - }) - : undefined; + }); /** * Adapted from Polymer app-localize-behavior. diff --git a/src/common/util/copy-clipboard.ts b/src/common/util/copy-clipboard.ts new file mode 100644 index 0000000000..9d7e6885fa --- /dev/null +++ b/src/common/util/copy-clipboard.ts @@ -0,0 +1,8 @@ +export const copyToClipboard = (str) => { + const el = document.createElement("textarea"); + el.value = str; + document.body.appendChild(el); + el.select(); + document.execCommand("copy"); + document.body.removeChild(el); +}; diff --git a/src/components/device/ha-area-devices-picker.ts b/src/components/device/ha-area-devices-picker.ts index 9a974c88f1..bfd61e7479 100644 --- a/src/components/device/ha-area-devices-picker.ts +++ b/src/components/device/ha-area-devices-picker.ts @@ -1,5 +1,5 @@ +import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-button/mwc-button"; -import "../ha-icon-button"; import "@polymer/paper-input/paper-input"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item-body"; @@ -38,6 +38,8 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { PolymerChangedEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; import "./ha-devices-picker"; +import "../ha-svg-icon"; +import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; interface DevicesByArea { [areaId: string]: AreaDevices; @@ -62,7 +64,7 @@ const rowRenderer = ( margin: -10px 0; padding: 0; } - ha-icon-button { + mwc-icon-button { float: right; } .devices { @@ -324,36 +326,34 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { autocorrect="off" spellcheck="false" > - ${this.value - ? html` - + ${this.value + ? html` - Clear - - ` - : ""} - ${areas.length > 0 - ? html` - - Toggle - - ` - : ""} + + ` + : ""} + ${areas.length > 0 + ? html` + + + + ` + : ""} +
ha-icon-button { - width: 24px; - height: 24px; - padding: 2px; + .suffix { + display: flex; + } + mwc-icon-button { + --mdc-icon-button-size: 24px; + padding: 0px 2px; color: var(--secondary-text-color); } [hidden] { diff --git a/src/components/entity/ha-chart-base.js b/src/components/entity/ha-chart-base.js index e02470bd08..09ebaef2c4 100644 --- a/src/components/entity/ha-chart-base.js +++ b/src/components/entity/ha-chart-base.js @@ -88,6 +88,7 @@ class HaChartBase extends mixinBehaviors( .chartTooltip .beforeBody { text-align: center; font-weight: 300; + word-break: break-all; } .chartLegend li { display: inline-block; diff --git a/src/components/entity/ha-entity-attribute-picker.ts b/src/components/entity/ha-entity-attribute-picker.ts index e9c3b52e0f..5b1dbfc080 100644 --- a/src/components/entity/ha-entity-attribute-picker.ts +++ b/src/components/entity/ha-entity-attribute-picker.ts @@ -19,6 +19,7 @@ import { PolymerChangedEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; import "../ha-svg-icon"; import "./state-badge"; +import { formatAttributeName } from "../../util/hass-attributes-util"; import "@material/mwc-icon-button/mwc-icon-button"; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; @@ -35,7 +36,9 @@ const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => { `; } - root.querySelector("paper-item")!.textContent = model.item; + root.querySelector("paper-item")!.textContent = formatAttributeName( + model.item + ); }; @customElement("ha-entity-attribute-picker") @@ -92,7 +95,7 @@ class HaEntityAttributePicker extends LitElement { this.hass.localize( "ui.components.entity.entity-attribute-picker.attribute" )} - .value=${this._value} + .value=${this._value ? formatAttributeName(this._value) : ""} .disabled=${this.disabled || !this.entityId} class="input" autocapitalize="none" @@ -140,7 +143,7 @@ class HaEntityAttributePicker extends LitElement { } private get _value() { - return this.value || ""; + return this.value; } private _openedChanged(ev: PolymerChangedEvent) { diff --git a/src/components/entity/ha-state-icon.js b/src/components/entity/ha-state-icon.js deleted file mode 100644 index 80a7ad22d1..0000000000 --- a/src/components/entity/ha-state-icon.js +++ /dev/null @@ -1,25 +0,0 @@ -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { stateIcon } from "../../common/entity/state_icon"; -import "../ha-icon"; - -class HaStateIcon extends PolymerElement { - static get template() { - return html` `; - } - - static get properties() { - return { - stateObj: { - type: Object, - }, - }; - } - - computeIcon(stateObj) { - return stateIcon(stateObj); - } -} - -customElements.define("ha-state-icon", HaStateIcon); diff --git a/src/components/entity/state-info.js b/src/components/entity/state-info.js index 36014db18e..d228de67de 100644 --- a/src/components/entity/state-info.js +++ b/src/components/entity/state-info.js @@ -59,6 +59,19 @@ class StateInfo extends LocalizeMixin(PolymerElement) { @apply --paper-font-common-nowrap; color: var(--secondary-text-color); } + + .row { + display: flex; + flex-direction: row; + flex-wrap: no-wrap; + width: 100%; + justify-content: space-between; + margin: 0 2px 4px 0; + } + + .row:last-child { + margin-bottom: 0px; + } `; } @@ -81,11 +94,26 @@ class StateInfo extends LocalizeMixin(PolymerElement) { datetime="[[stateObj.last_changed]]" > - [[localize('ui.dialogs.more_info_control.last_updated')]]: - +
+
+ + [[localize('ui.dialogs.more_info_control.last_changed')]]: + + +
+
+ + [[localize('ui.dialogs.more_info_control.last_updated')]]: + + +
+
diff --git a/src/components/ha-attributes.ts b/src/components/ha-attributes.ts index 892148e664..f51d006072 100644 --- a/src/components/ha-attributes.ts +++ b/src/components/ha-attributes.ts @@ -9,7 +9,9 @@ import { TemplateResult, } from "lit-element"; import { until } from "lit-html/directives/until"; -import hassAttributeUtil from "../util/hass-attributes-util"; +import hassAttributeUtil, { + formatAttributeName, +} from "../util/hass-attributes-util"; let jsYamlPromise: Promise; @@ -34,7 +36,7 @@ class HaAttributes extends LitElement { (attribute) => html`
- ${attribute.replace(/_/g, " ").replace(/\bid\b/g, "ID")} + ${formatAttributeName(attribute)}
${this.formatAttribute(attribute)} @@ -61,15 +63,16 @@ class HaAttributes extends LitElement { justify-content: space-between; } .data-entry .value { - max-width: 200px; + max-width: 50%; overflow-wrap: break-word; + text-align: right; } - .key:first-letter { - text-transform: capitalize; + .key { + flex-grow: 1; } .attribution { color: var(--secondary-text-color); - text-align: right; + text-align: center; } pre { font-family: inherit; diff --git a/src/components/ha-circular-progress.ts b/src/components/ha-circular-progress.ts index 1e70908291..c3e188d267 100644 --- a/src/components/ha-circular-progress.ts +++ b/src/components/ha-circular-progress.ts @@ -11,7 +11,7 @@ export class HaCircularProgress extends CircularProgress { public alt = "Loading"; @property() - public size: "small" | "medium" | "large" = "medium"; + public size: "tiny" | "small" | "medium" | "large" = "medium"; // @ts-ignore public set density(_) { @@ -20,6 +20,8 @@ export class HaCircularProgress extends CircularProgress { public get density() { switch (this.size) { + case "tiny": + return -8; case "small": return -5; case "medium": diff --git a/src/components/ha-climate-state.js b/src/components/ha-climate-state.js deleted file mode 100644 index eb961cce4c..0000000000 --- a/src/components/ha-climate-state.js +++ /dev/null @@ -1,131 +0,0 @@ -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { CLIMATE_PRESET_NONE } from "../data/climate"; -import LocalizeMixin from "../mixins/localize-mixin"; - -/* - * @appliesMixin LocalizeMixin - */ -class HaClimateState extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - - -
- -
[[computeTarget(hass, stateObj)]]
-
- - - `; - } - - static get properties() { - return { - hass: Object, - stateObj: Object, - currentStatus: { - type: String, - computed: "computeCurrentStatus(hass, stateObj)", - }, - }; - } - - computeCurrentStatus(hass, stateObj) { - if (!hass || !stateObj) return null; - if (stateObj.attributes.current_temperature != null) { - return `${stateObj.attributes.current_temperature} ${hass.config.unit_system.temperature}`; - } - if (stateObj.attributes.current_humidity != null) { - return `${stateObj.attributes.current_humidity} %`; - } - return null; - } - - computeTarget(hass, stateObj) { - if (!hass || !stateObj) return null; - // We're using "!= null" on purpose so that we match both null and undefined. - if ( - stateObj.attributes.target_temp_low != null && - stateObj.attributes.target_temp_high != null - ) { - return `${stateObj.attributes.target_temp_low}-${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`; - } - if (stateObj.attributes.temperature != null) { - return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`; - } - if ( - stateObj.attributes.target_humidity_low != null && - stateObj.attributes.target_humidity_high != null - ) { - return `${stateObj.attributes.target_humidity_low}-${stateObj.attributes.target_humidity_high}%`; - } - if (stateObj.attributes.humidity != null) { - return `${stateObj.attributes.humidity} %`; - } - - return ""; - } - - _hasKnownState(state) { - return state !== "unknown"; - } - - _localizeState(localize, stateObj) { - const stateString = localize(`component.climate.state._.${stateObj.state}`); - return stateObj.attributes.hvac_action - ? `${localize( - `state_attributes.climate.hvac_action.${stateObj.attributes.hvac_action}` - )} (${stateString})` - : stateString; - } - - _localizePreset(localize, preset) { - return localize(`state_attributes.climate.preset_mode.${preset}`) || preset; - } - - _renderPreset(attributes) { - return ( - attributes.preset_mode && attributes.preset_mode !== CLIMATE_PRESET_NONE - ); - } -} -customElements.define("ha-climate-state", HaClimateState); diff --git a/src/components/ha-climate-state.ts b/src/components/ha-climate-state.ts new file mode 100644 index 0000000000..1aa49ae6b2 --- /dev/null +++ b/src/components/ha-climate-state.ts @@ -0,0 +1,139 @@ +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { HassEntity } from "home-assistant-js-websocket"; + +import { CLIMATE_PRESET_NONE } from "../data/climate"; +import type { HomeAssistant } from "../types"; + +@customElement("ha-climate-state") +class HaClimateState extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: HassEntity; + + protected render(): TemplateResult { + const currentStatus = this._computeCurrentStatus(); + + return html`
+ ${this.stateObj.state !== "unknown" + ? html` + ${this._localizeState()} + ${this.stateObj.attributes.preset_mode && + this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE + ? html`- + ${this.hass.localize( + `state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}` + ) || this.stateObj.attributes.preset_mode}` + : ""} + ` + : ""} +
${this._computeTarget()}
+
+ + ${currentStatus + ? html`
+ ${this.hass.localize("ui.card.climate.currently")}: +
${currentStatus}
+
` + : ""}`; + } + + private _computeCurrentStatus(): string | undefined { + if (!this.hass || !this.stateObj) { + return undefined; + } + + if (this.stateObj.attributes.current_temperature != null) { + return `${this.stateObj.attributes.current_temperature} ${this.hass.config.unit_system.temperature}`; + } + + if (this.stateObj.attributes.current_humidity != null) { + return `${this.stateObj.attributes.current_humidity} %`; + } + + return undefined; + } + + private _computeTarget(): string { + if (!this.hass || !this.stateObj) { + return ""; + } + + if ( + this.stateObj.attributes.target_temp_low != null && + this.stateObj.attributes.target_temp_high != null + ) { + return `${this.stateObj.attributes.target_temp_low}-${this.stateObj.attributes.target_temp_high} ${this.hass.config.unit_system.temperature}`; + } + + if (this.stateObj.attributes.temperature != null) { + return `${this.stateObj.attributes.temperature} ${this.hass.config.unit_system.temperature}`; + } + if ( + this.stateObj.attributes.target_humidity_low != null && + this.stateObj.attributes.target_humidity_high != null + ) { + return `${this.stateObj.attributes.target_humidity_low}-${this.stateObj.attributes.target_humidity_high}%`; + } + + if (this.stateObj.attributes.humidity != null) { + return `${this.stateObj.attributes.humidity} %`; + } + + return ""; + } + + private _localizeState(): string { + const stateString = this.hass.localize( + `component.climate.state._.${this.stateObj.state}` + ); + + return this.stateObj.attributes.hvac_action + ? `${this.hass.localize( + `state_attributes.climate.hvac_action.${this.stateObj.attributes.hvac_action}` + )} (${stateString})` + : stateString; + } + + static get styles(): CSSResult { + return css` + :host { + display: flex; + flex-direction: column; + justify-content: center; + white-space: nowrap; + } + + .target { + color: var(--primary-text-color); + } + + .current { + color: var(--secondary-text-color); + } + + .state-label { + font-weight: bold; + text-transform: capitalize; + } + + .unit { + display: inline-block; + direction: ltr; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-climate-state": HaClimateState; + } +} diff --git a/src/components/ha-cover-tilt-controls.js b/src/components/ha-cover-tilt-controls.js deleted file mode 100644 index 08f992380a..0000000000 --- a/src/components/ha-cover-tilt-controls.js +++ /dev/null @@ -1,106 +0,0 @@ -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import "./ha-icon-button"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { UNAVAILABLE } from "../data/entity"; -import CoverEntity from "../util/cover-model"; - -class HaCoverTiltControls extends PolymerElement { - static get template() { - return html` - - - - - - `; - } - - static get properties() { - return { - hass: { - type: Object, - }, - stateObj: { - type: Object, - }, - entityObj: { - type: Object, - computed: "computeEntityObj(hass, stateObj)", - }, - }; - } - - computeEntityObj(hass, stateObj) { - return new CoverEntity(hass, stateObj); - } - - computeStopDisabled(stateObj) { - if (stateObj.state === UNAVAILABLE) { - return true; - } - return false; - } - - computeOpenDisabled(stateObj, entityObj) { - if (stateObj.state === UNAVAILABLE) { - return true; - } - const assumedState = stateObj.attributes.assumed_state === true; - return entityObj.isFullyOpenTilt && !assumedState; - } - - computeClosedDisabled(stateObj, entityObj) { - if (stateObj.state === UNAVAILABLE) { - return true; - } - const assumedState = stateObj.attributes.assumed_state === true; - return entityObj.isFullyClosedTilt && !assumedState; - } - - onOpenTiltTap(ev) { - ev.stopPropagation(); - this.entityObj.openCoverTilt(); - } - - onCloseTiltTap(ev) { - ev.stopPropagation(); - this.entityObj.closeCoverTilt(); - } - - onStopTiltTap(ev) { - ev.stopPropagation(); - this.entityObj.stopCoverTilt(); - } -} - -customElements.define("ha-cover-tilt-controls", HaCoverTiltControls); diff --git a/src/components/ha-cover-tilt-controls.ts b/src/components/ha-cover-tilt-controls.ts new file mode 100644 index 0000000000..843eaa6598 --- /dev/null +++ b/src/components/ha-cover-tilt-controls.ts @@ -0,0 +1,122 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { + LitElement, + property, + internalProperty, + CSSResult, + css, + customElement, + TemplateResult, + html, + PropertyValues, +} from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; + +import { UNAVAILABLE } from "../data/entity"; +import { HomeAssistant } from "../types"; +import CoverEntity from "../util/cover-model"; + +import "./ha-icon-button"; + +@customElement("ha-cover-tilt-controls") +class HaCoverTiltControls extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) stateObj!: HassEntity; + + @internalProperty() private _entityObj?: CoverEntity; + + protected updated(changedProperties: PropertyValues): void { + super.updated(changedProperties); + + if (changedProperties.has("stateObj")) { + this._entityObj = new CoverEntity(this.hass, this.stateObj); + } + } + + protected render(): TemplateResult { + if (!this._entityObj) { + return html``; + } + + return html` + + `; + } + + private _computeOpenDisabled(): boolean { + if (this.stateObj.state === UNAVAILABLE) { + return true; + } + const assumedState = this.stateObj.attributes.assumed_state === true; + return this._entityObj.isFullyOpenTilt && !assumedState; + } + + private _computeClosedDisabled(): boolean { + if (this.stateObj.state === UNAVAILABLE) { + return true; + } + const assumedState = this.stateObj.attributes.assumed_state === true; + return this._entityObj.isFullyClosedTilt && !assumedState; + } + + private _onOpenTiltTap(ev): void { + ev.stopPropagation(); + this._entityObj.openCoverTilt(); + } + + private _onCloseTiltTap(ev): void { + ev.stopPropagation(); + this._entityObj.closeCoverTilt(); + } + + private _onStopTiltTap(ev): void { + ev.stopPropagation(); + this._entityObj.stopCoverTilt(); + } + + static get styles(): CSSResult { + return css` + :host { + white-space: nowrap; + } + .invisible { + visibility: hidden !important; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-cover-tilt-controls": HaCoverTiltControls; + } +} diff --git a/src/components/ha-date-input.ts b/src/components/ha-date-input.ts index 7504d38dcb..bdccf06cb3 100644 --- a/src/components/ha-date-input.ts +++ b/src/components/ha-date-input.ts @@ -1,127 +1,70 @@ -import "@polymer/paper-input/paper-input"; -import type { PaperInputElement } from "@polymer/paper-input/paper-input"; -import { - css, - customElement, - html, - LitElement, - property, - TemplateResult, -} from "lit-element"; +import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker"; -@customElement("ha-date-input") -export class HaDateInput extends LitElement { - @property() public year?: string; +const VaadinDatePicker = customElements.get("vaadin-date-picker"); - @property() public month?: string; +export class HaDateInput extends VaadinDatePicker { + constructor() { + super(); - @property() public day?: string; + this.i18n.formatDate = this._formatISODate; + this.i18n.parseDate = this._parseISODate; + } - @property({ type: Boolean }) public disabled = false; - - static get styles() { - return css` + ready() { + super.ready(); + const styleEl = document.createElement("style"); + styleEl.innerHTML = ` :host { - display: block; - font-family: var(--paper-font-common-base_-_font-family); - -webkit-font-smoothing: var( - --paper-font-common-base_-_-webkit-font-smoothing - ); - } - - paper-input { - width: 30px; - text-align: center; - --paper-input-container-shared-input-style_-_-webkit-appearance: textfield; - --paper-input-container-input_-_-moz-appearance: textfield; - --paper-input-container-shared-input-style_-_appearance: textfield; - --paper-input-container-input-webkit-spinner_-_-webkit-appearance: none; - --paper-input-container-input-webkit-spinner_-_margin: 0; - --paper-input-container-input-webkit-spinner_-_display: none; - } - - paper-input#year { - width: 50px; - } - - .date-input-wrap { - display: flex; - flex-direction: row; + width: 12ex; + margin-top: -6px; + --material-body-font-size: 16px; + --_material-text-field-input-line-background-color: var(--primary-text-color); + --_material-text-field-input-line-opacity: 1; + --material-primary-color: var(--primary-text-color); } `; + this.shadowRoot.appendChild(styleEl); + this._inputElement.querySelector("[part='toggle-button']").style.display = + "none"; } - protected render(): TemplateResult { - return html` -
- - - - - - - - - - -
- `; + private _formatISODate(d) { + return [ + ("0000" + String(d.year)).slice(-4), + ("0" + String(d.month + 1)).slice(-2), + ("0" + String(d.day)).slice(-2), + ].join("-"); } - private _formatYear() { - const yearElement = this.shadowRoot!.getElementById( - "year" - ) as PaperInputElement; - this.year = yearElement.value!; - } + private _parseISODate(text) { + const parts = text.split("-"); + const today = new Date(); + let date; + let month = today.getMonth(); + let year = today.getFullYear(); + if (parts.length === 3) { + year = parseInt(parts[0]); + if (parts[0].length < 3 && year >= 0) { + year += year < 50 ? 2000 : 1900; + } + month = parseInt(parts[1]) - 1; + date = parseInt(parts[2]); + } else if (parts.length === 2) { + month = parseInt(parts[0]) - 1; + date = parseInt(parts[1]); + } else if (parts.length === 1) { + date = parseInt(parts[0]); + } - private _formatMonth() { - const monthElement = this.shadowRoot!.getElementById( - "month" - ) as PaperInputElement; - this.month = ("0" + monthElement.value!).slice(-2); - } - - private _formatDay() { - const dayElement = this.shadowRoot!.getElementById( - "day" - ) as PaperInputElement; - this.day = ("0" + dayElement.value!).slice(-2); - } - - get value() { - return `${this.year}-${this.month}-${this.day}`; + if (date !== undefined) { + return { day: date, month, year }; + } + return undefined; } } +customElements.define("ha-date-input", HaDateInput as any); + declare global { interface HTMLElementTagNameMap { "ha-date-input": HaDateInput; diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index c94fbb2115..75676269f7 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -40,6 +40,7 @@ export class HaDialog extends MwcDialog { .mdc-dialog { --mdc-dialog-scroll-divider-color: var(--divider-color); z-index: var(--dialog-z-index, 7); + backdrop-filter: var(--dialog-backdrop-filter, none); } .mdc-dialog__actions { justify-content: var(--justify-action-buttons, flex-end); diff --git a/src/components/ha-hls-player.ts b/src/components/ha-hls-player.ts index 5b84c8d4e2..05e7e6cdc9 100644 --- a/src/components/ha-hls-player.ts +++ b/src/components/ha-hls-player.ts @@ -104,7 +104,6 @@ class HaHLSPlayer extends LitElement { private async _startHls(): Promise { const videoEl = this._videoEl; - const playlist_url = this.url.replace("master_playlist", "playlist"); const useExoPlayerPromise = this._getUseExoPlayer(); const masterPlaylistPromise = fetch(this.url); @@ -126,13 +125,30 @@ class HaHLSPlayer extends LitElement { } this._useExoPlayer = await useExoPlayerPromise; - let hevcRegexp: RegExp; - let masterPlaylist: string; - if (this._useExoPlayer) { - hevcRegexp = /CODECS=".*?((hev1)|(hvc1))\..*?"/; - masterPlaylist = await (await masterPlaylistPromise).text(); + const masterPlaylist = await (await masterPlaylistPromise).text(); + + // Parse playlist assuming it is a master playlist. Match group 1 is whether hevc, match group 2 is regular playlist url + // See https://tools.ietf.org/html/rfc8216 for HLS spec details + const playlistRegexp = /#EXT-X-STREAM-INF:.*?(?:CODECS=".*?(?hev1|hvc1)?\..*?".*?)?(?:\n|\r\n)(?.+)/g; + const match = playlistRegexp.exec(masterPlaylist); + const matchTwice = playlistRegexp.exec(masterPlaylist); + + // Get the regular playlist url from the input (master) playlist, falling back to the input playlist if necessary + // This avoids the player having to load and parse the master playlist again before loading the regular playlist + let playlist_url: string; + if (match !== null && matchTwice === null) { + // Only send the regular playlist url if we match exactly once + playlist_url = new URL(match.groups!.streamUrl, this.url).href; + } else { + playlist_url = this.url; } - if (this._useExoPlayer && hevcRegexp!.test(masterPlaylist!)) { + + // If codec is HEVC and ExoPlayer is supported, use ExoPlayer. + if ( + this._useExoPlayer && + match !== null && + match.groups!.isHevc !== undefined + ) { this._renderHLSExoPlayer(playlist_url); } else if (hls.isSupported()) { this._renderHLSPolyfill(videoEl, hls, playlist_url); diff --git a/src/components/ha-icon-button-arrow-next.ts b/src/components/ha-icon-button-arrow-next.ts index 4d84add488..fa2917db6a 100644 --- a/src/components/ha-icon-button-arrow-next.ts +++ b/src/components/ha-icon-button-arrow-next.ts @@ -9,11 +9,16 @@ import { import { mdiArrowLeft, mdiArrowRight } from "@mdi/js"; import "@material/mwc-icon-button/mwc-icon-button"; import "./ha-svg-icon"; +import { HomeAssistant } from "../types"; @customElement("ha-icon-button-arrow-next") export class HaIconButtonArrowNext extends LitElement { + @property({ attribute: false }) public hass?: HomeAssistant; + @property({ type: Boolean }) public disabled = false; + @property() public label?: string; + @internalProperty() private _icon = mdiArrowRight; public connectedCallback() { @@ -29,7 +34,10 @@ export class HaIconButtonArrowNext extends LitElement { } protected render(): TemplateResult { - return html` + return html` `; } diff --git a/src/components/ha-icon-button-arrow-prev.ts b/src/components/ha-icon-button-arrow-prev.ts index 07c8265bb2..c7fbada10d 100644 --- a/src/components/ha-icon-button-arrow-prev.ts +++ b/src/components/ha-icon-button-arrow-prev.ts @@ -9,11 +9,16 @@ import { import { mdiArrowLeft, mdiArrowRight } from "@mdi/js"; import "@material/mwc-icon-button/mwc-icon-button"; import "./ha-svg-icon"; +import { HomeAssistant } from "../types"; @customElement("ha-icon-button-arrow-prev") export class HaIconButtonArrowPrev extends LitElement { + @property({ attribute: false }) public hass?: HomeAssistant; + @property({ type: Boolean }) public disabled = false; + @property() public label?: string; + @internalProperty() private _icon = mdiArrowLeft; public connectedCallback() { @@ -29,9 +34,14 @@ export class HaIconButtonArrowPrev extends LitElement { } protected render(): TemplateResult { - return html` - - `; + return html` + + + + `; } } diff --git a/src/components/ha-icon-button-next.ts b/src/components/ha-icon-button-next.ts index e328c4ff1e..48eca31a4a 100644 --- a/src/components/ha-icon-button-next.ts +++ b/src/components/ha-icon-button-next.ts @@ -9,11 +9,16 @@ import { import { mdiChevronRight, mdiChevronLeft } from "@mdi/js"; import "@material/mwc-icon-button"; import "./ha-svg-icon"; +import { HomeAssistant } from "../types"; @customElement("ha-icon-button-next") export class HaIconButtonNext extends LitElement { + @property({ attribute: false }) public hass?: HomeAssistant; + @property({ type: Boolean }) public disabled = false; + @property() public label?: string; + @internalProperty() private _icon = mdiChevronRight; public connectedCallback() { @@ -29,9 +34,14 @@ export class HaIconButtonNext extends LitElement { } protected render(): TemplateResult { - return html` - - `; + return html` + + + + `; } } diff --git a/src/components/ha-icon-button-prev.ts b/src/components/ha-icon-button-prev.ts index 077b5a03bf..8b88afedda 100644 --- a/src/components/ha-icon-button-prev.ts +++ b/src/components/ha-icon-button-prev.ts @@ -9,11 +9,16 @@ import { import { mdiChevronRight, mdiChevronLeft } from "@mdi/js"; import "@material/mwc-icon-button/mwc-icon-button"; import "./ha-svg-icon"; +import { HomeAssistant } from "../types"; @customElement("ha-icon-button-prev") export class HaIconButtonPrev extends LitElement { + @property({ attribute: false }) public hass?: HomeAssistant; + @property({ type: Boolean }) public disabled = false; + @property() public label?: string; + @internalProperty() private _icon = mdiChevronLeft; public connectedCallback() { @@ -29,9 +34,14 @@ export class HaIconButtonPrev extends LitElement { } protected render(): TemplateResult { - return html` - - `; + return html` + + + + `; } } diff --git a/src/components/ha-labeled-slider.js b/src/components/ha-labeled-slider.js index 147fd5b1b1..4d15c37b5e 100644 --- a/src/components/ha-labeled-slider.js +++ b/src/components/ha-labeled-slider.js @@ -14,7 +14,7 @@ class HaLabeledSlider extends PolymerElement { } .title { - margin-bottom: 8px; + margin: 4px 0 8px; color: var(--primary-text-color); } diff --git a/src/components/ha-markdown-element.ts b/src/components/ha-markdown-element.ts index b6e78c99b9..cd07cf0a01 100644 --- a/src/components/ha-markdown-element.ts +++ b/src/components/ha-markdown-element.ts @@ -47,7 +47,6 @@ class HaMarkdownElement extends UpdatingElement { node.host !== document.location.host ) { node.target = "_blank"; - node.rel = "noreferrer"; // protect referrer on external links and deny window.opener access for security reasons // (see https://mathiasbynens.github.io/rel-noopener/) diff --git a/src/components/ha-push-notifications-toggle.js b/src/components/ha-push-notifications-toggle.js index 61aec06691..b8bead07fc 100644 --- a/src/components/ha-push-notifications-toggle.js +++ b/src/components/ha-push-notifications-toggle.js @@ -3,6 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import { getAppKey } from "../data/notify_html5"; import { EventsMixin } from "../mixins/events-mixin"; +import { showPromptDialog } from "../dialogs/generic/show-dialog-box"; import "./ha-switch"; export const pushSupported = @@ -88,7 +89,14 @@ class HaPushNotificationsToggle extends EventsMixin(PolymerElement) { browserName = "chrome"; } - const name = prompt("What should this device be called ?"); + const name = await showPromptDialog(this, { + title: this.hass.localize( + "ui.panel.profile.push_notifications.add_device_prompt.title" + ), + inputLabel: this.hass.localize( + "ui.panel.profile.push_notifications.add_device_prompt.input_label" + ), + }); if (name == null) { this.pushChecked = false; return; diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 2f1f473ca4..7a4791b967 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -202,195 +202,17 @@ class HaSidebar extends LitElement { private _sortable?; protected render() { - const hass = this.hass; - - if (!hass) { + if (!this.hass) { return html``; } - const [beforeSpacer, afterSpacer] = computePanels( - hass.panels, - hass.defaultPanel, - this._panelOrder, - this._hiddenPanels - ); - - let notificationCount = this._notifications - ? this._notifications.length - : 0; - for (const entityId in hass.states) { - if (computeDomain(entityId) === "configurator") { - notificationCount++; - } - } - + // prettier-ignore return html` - - - ${this.editMode - ? html`
- ${guard([this._hiddenPanels, this._renderEmptySortable], () => - this._renderEmptySortable - ? "" - : this._renderPanels(beforeSpacer) - )} -
` - : this._renderPanels(beforeSpacer)} -
- ${this.editMode && this._hiddenPanels.length - ? html` - ${this._hiddenPanels.map((url) => { - const panel = this.hass.panels[url]; - if (!panel) { - return ""; - } - return html` - - ${panel.url_path === this.hass.defaultPanel - ? hass.localize("panel.states") - : hass.localize(`panel.${panel.title}`) || - panel.title} - - - - `; - })} -
- ` - : ""} - ${this._renderPanels(afterSpacer)} - ${this._externalConfig && this._externalConfig.hasSettingsScreen - ? html` - - - - - ${hass.localize("ui.sidebar.external_app_configuration")} - - - - ` - : ""} -
- -
- -
- - - ${!this.expanded && notificationCount > 0 - ? html` - - ${notificationCount} - - ` - : ""} - - ${hass.localize("ui.notification_drawer.title")} - - ${this.expanded && notificationCount > 0 - ? html` - ${notificationCount} - ` - : ""} - -
- - - - - - - ${hass.user ? hass.user.name : ""} - - - + ${this._renderHeader()} + ${this._renderAllPanels()} + ${this._renderDivider()} + ${this._renderNotifications()} + ${this._renderUserItem()}
`; @@ -475,6 +297,213 @@ class HaSidebar extends LitElement { } } + private _renderHeader() { + return html``; + } + + private _renderAllPanels() { + const [beforeSpacer, afterSpacer] = computePanels( + this.hass.panels, + this.hass.defaultPanel, + this._panelOrder, + this._hiddenPanels + ); + + // prettier-ignore + return html` + + ${this.editMode + ? this._renderPanelsEdit(beforeSpacer) + : this._renderPanels(beforeSpacer)} + ${this._renderSpacer()} + ${this._renderPanels(afterSpacer)} + ${this._renderExternalConfiguration()} + + `; + } + + private _renderPanelsEdit(beforeSpacer: PanelInfo[]) { + // prettier-ignore + return html`
+ ${guard([this._hiddenPanels, this._renderEmptySortable], () => + this._renderEmptySortable ? "" : this._renderPanels(beforeSpacer) + )} +
+ ${this._renderSpacer()} + ${this._renderHiddenPanels()} `; + } + + private _renderHiddenPanels() { + return html` ${this._hiddenPanels.length + ? html`${this._hiddenPanels.map((url) => { + const panel = this.hass.panels[url]; + if (!panel) { + return ""; + } + return html` + + ${panel.url_path === this.hass.defaultPanel + ? this.hass.localize("panel.states") + : this.hass.localize(`panel.${panel.title}`) || + panel.title} + + + + `; + })} + ${this._renderSpacer()}` + : ""}`; + } + + private _renderDivider() { + return html`
`; + } + + private _renderSpacer() { + return html`
`; + } + + private _renderNotifications() { + let notificationCount = this._notifications + ? this._notifications.length + : 0; + for (const entityId in this.hass.states) { + if (computeDomain(entityId) === "configurator") { + notificationCount++; + } + } + + return html`
+ + + ${!this.expanded && notificationCount > 0 + ? html` + + ${notificationCount} + + ` + : ""} + + ${this.hass.localize("ui.notification_drawer.title")} + + ${this.expanded && notificationCount > 0 + ? html` ${notificationCount} ` + : ""} + +
`; + } + + private _renderUserItem() { + return html` + + + + + ${this.hass.user ? this.hass.user.name : ""} + + + `; + } + + private _renderExternalConfiguration() { + return html`${this._externalConfig && this._externalConfig.hasSettingsScreen + ? html` + + + + + ${this.hass.localize("ui.sidebar.external_app_configuration")} + + + + ` + : ""}`; + } + private get _tooltip() { return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement; } @@ -725,7 +754,7 @@ class HaSidebar extends LitElement { -moz-user-select: none; border-right: 1px solid var(--divider-color); background-color: var(--sidebar-background-color); - width: 64px; + width: 56px; } :host([expanded]) { width: 256px; @@ -737,8 +766,9 @@ class HaSidebar extends LitElement { } .menu { height: var(--header-height); + box-sizing: border-box; display: flex; - padding: 0 8.5px; + padding: 0 4px; border-bottom: 1px solid transparent; white-space: nowrap; font-weight: 400; @@ -747,11 +777,11 @@ class HaSidebar extends LitElement { background-color: var(--primary-background-color); font-size: 20px; align-items: center; - padding-left: calc(8.5px + env(safe-area-inset-left)); + padding-left: calc(4px + env(safe-area-inset-left)); } :host([rtl]) .menu { - padding-left: 8.5px; - padding-right: calc(8.5px + env(safe-area-inset-right)); + padding-left: 4px; + padding-right: calc(4px + env(safe-area-inset-right)); } :host([expanded]) .menu { width: calc(256px + env(safe-area-inset-left)); @@ -762,26 +792,27 @@ class HaSidebar extends LitElement { .menu mwc-icon-button { color: var(--sidebar-icon-color); } - :host([expanded]) .menu mwc-icon-button { - margin-right: 23px; - } - :host([expanded][rtl]) .menu mwc-icon-button { - margin-right: 0px; - margin-left: 23px; - } - .title { + margin-left: 19px; width: 100%; display: none; } + :host([rtl]) .title { + margin-left: 0; + margin-right: 19px; + } :host([narrow]) .title { + margin: 0; padding: 0 16px; } :host([expanded]) .title { display: initial; } - .title mwc-button { - width: 90%; + :host([expanded]) .menu mwc-button { + margin: 0 8px; + } + .menu mwc-button { + width: 100%; } #sortable, .hidden-panel { @@ -819,14 +850,14 @@ class HaSidebar extends LitElement { paper-icon-item { box-sizing: border-box; - margin: 4px 8px; + margin: 4px; padding-left: 12px; border-radius: 4px; --paper-item-min-height: 40px; width: 48px; } :host([expanded]) paper-icon-item { - width: 240px; + width: 248px; } :host([rtl]) paper-icon-item { padding-left: auto; @@ -843,9 +874,9 @@ class HaSidebar extends LitElement { border-radius: 4px; position: absolute; top: 0; - right: 0; + right: 2px; bottom: 0; - left: 0; + left: 2px; pointer-events: none; content: ""; transition: opacity 15ms linear; diff --git a/src/components/ha-tab.ts b/src/components/ha-tab.ts index cc052052d9..4e0f33758f 100644 --- a/src/components/ha-tab.ts +++ b/src/components/ha-tab.ts @@ -99,13 +99,13 @@ export class HaTab extends LitElement { display: flex; flex-direction: column; text-align: center; + box-sizing: border-box; align-items: center; justify-content: center; - height: 64px; + height: var(--header-height); cursor: pointer; position: relative; outline: none; - box-sizing: border-box; } .name { diff --git a/src/components/ha-tabs.ts b/src/components/ha-tabs.ts index b6dc45dcf1..f28f8d7766 100644 --- a/src/components/ha-tabs.ts +++ b/src/components/ha-tabs.ts @@ -34,8 +34,8 @@ export class HaTabs extends PaperTabs { superStyle!.appendChild( document.createTextNode(` - :host { - padding-top: .5px; + #selectionBar { + box-sizing: border-box; } .not-visible { display: none; diff --git a/src/components/media-player/ha-media-player-browse.ts b/src/components/media-player/ha-media-player-browse.ts index 8c17267fc4..93a5ca25e4 100644 --- a/src/components/media-player/ha-media-player-browse.ts +++ b/src/components/media-player/ha-media-player-browse.ts @@ -540,17 +540,20 @@ export class HaMediaPlayerBrowse extends LitElement { mediaContentType?: string ): Promise { this._loading = true; - const itemData = - this.entityId !== BROWSER_PLAYER - ? await browseMediaPlayer( - this.hass, - this.entityId, - mediaContentId, - mediaContentType - ) - : await browseLocalMediaPlayer(this.hass, mediaContentId); - - this._loading = false; + let itemData: any; + try { + itemData = + this.entityId !== BROWSER_PLAYER + ? await browseMediaPlayer( + this.hass, + this.entityId, + mediaContentId, + mediaContentType + ) + : await browseLocalMediaPlayer(this.hass, mediaContentId); + } finally { + this._loading = false; + } return itemData; } diff --git a/src/data/data_entry_flow.ts b/src/data/data_entry_flow.ts index 189d13336e..a9a7afbf69 100644 --- a/src/data/data_entry_flow.ts +++ b/src/data/data_entry_flow.ts @@ -58,8 +58,18 @@ export interface DataEntryFlowStepAbort { description_placeholders: { [key: string]: string }; } +export interface DataEntryFlowStepProgress { + type: "progress"; + flow_id: string; + handler: string; + step_id: string; + progress_action: string; + description_placeholders: { [key: string]: string }; +} + export type DataEntryFlowStep = | DataEntryFlowStepForm | DataEntryFlowStepExternal | DataEntryFlowStepCreateEntry - | DataEntryFlowStepAbort; + | DataEntryFlowStepAbort + | DataEntryFlowStepProgress; diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 4c5271e82d..98ff504f22 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -20,6 +20,12 @@ export interface ExtEntityRegistryEntry extends EntityRegistryEntry { original_icon?: string; } +export interface UpdateEntityRegistryEntryResult { + entity_entry: ExtEntityRegistryEntry; + reload_delay?: number; + require_restart?: boolean; +} + export interface EntityRegistryEntryUpdateParams { name?: string | null; icon?: string | null; @@ -72,7 +78,7 @@ export const updateEntityRegistryEntry = ( hass: HomeAssistant, entityId: string, updates: Partial -): Promise => +): Promise => hass.callWS({ type: "config/entity_registry/update", entity_id: entityId, diff --git a/src/data/hassio/ingress.ts b/src/data/hassio/ingress.ts new file mode 100644 index 0000000000..ced84a2698 --- /dev/null +++ b/src/data/hassio/ingress.ts @@ -0,0 +1,26 @@ +import { HomeAssistant } from "../../types"; +import { HassioResponse } from "./common"; +import { CreateSessionResponse } from "./supervisor"; + +export const createHassioSession = async (hass: HomeAssistant) => { + const response = await hass.callApi>( + "POST", + "hassio/ingress/session" + ); + document.cookie = `ingress_session=${ + response.data.session + };path=/api/hassio_ingress/;SameSite=Strict${ + location.protocol === "https:" ? ";Secure" : "" + }`; + return response.data.session; +}; + +export const validateHassioSession = async ( + hass: HomeAssistant, + session: string +) => + await hass.callApi>( + "POST", + "hassio/ingress/validate_session", + { session } + ); diff --git a/src/data/hassio/supervisor.ts b/src/data/hassio/supervisor.ts index 9d2845f776..b21039bb1b 100644 --- a/src/data/hassio/supervisor.ts +++ b/src/data/hassio/supervisor.ts @@ -111,18 +111,6 @@ export const fetchHassioLogs = async ( return hass.callApi("GET", `hassio/${provider}/logs`); }; -export const createHassioSession = async (hass: HomeAssistant) => { - const response = await hass.callApi>( - "POST", - "hassio/ingress/session" - ); - document.cookie = `ingress_session=${ - response.data.session - };path=/api/hassio_ingress/;SameSite=Strict${ - location.protocol === "https:" ? ";Secure" : "" - }`; -}; - export const setSupervisorOption = async ( hass: HomeAssistant, data: SupervisorOptions diff --git a/src/data/media-player.ts b/src/data/media-player.ts index 393c06cb71..45bf6010ee 100644 --- a/src/data/media-player.ts +++ b/src/data/media-player.ts @@ -18,6 +18,8 @@ import { } from "@mdi/js"; import type { HassEntity } from "home-assistant-js-websocket"; import type { HomeAssistant } from "../types"; +import { UNAVAILABLE_STATES } from "./entity"; +import { supportsFeature } from "../common/entity/supports-feature"; export const SUPPORT_PAUSE = 1; export const SUPPORT_SEEK = 2; @@ -31,7 +33,7 @@ export const SUPPORT_PLAY_MEDIA = 512; export const SUPPORT_VOLUME_BUTTONS = 1024; export const SUPPORT_SELECT_SOURCE = 2048; export const SUPPORT_STOP = 4096; -export const SUPPORTS_PLAY = 16384; +export const SUPPORT_PLAY = 16384; export const SUPPORT_SELECT_SOUND_MODE = 65536; export const SUPPORT_BROWSE_MEDIA = 131072; export const CONTRAST_RATIO = 4.5; @@ -166,6 +168,7 @@ export const computeMediaDescription = (stateObj: HassEntity): string => { switch (stateObj.attributes.media_content_type) { case "music": + case "image": secondaryTitle = stateObj.attributes.media_artist; break; case "playlist": @@ -187,3 +190,85 @@ export const computeMediaDescription = (stateObj: HassEntity): string => { return secondaryTitle; }; + +export const computeMediaControls = ( + stateObj: HassEntity +): ControlButton[] | undefined => { + if (!stateObj) { + return undefined; + } + + const state = stateObj.state; + + if (UNAVAILABLE_STATES.includes(state)) { + return undefined; + } + + if (state === "off") { + return supportsFeature(stateObj, SUPPORT_TURN_ON) + ? [ + { + icon: "hass:power", + action: "turn_on", + }, + ] + : undefined; + } + + const buttons: ControlButton[] = []; + + if (supportsFeature(stateObj, SUPPORT_TURN_OFF)) { + buttons.push({ + icon: "hass:power", + action: "turn_off", + }); + } + + if ( + (state === "playing" || state === "paused") && + supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK) + ) { + buttons.push({ + icon: "hass:skip-previous", + action: "media_previous_track", + }); + } + + if ( + (state === "playing" && + (supportsFeature(stateObj, SUPPORT_PAUSE) || + supportsFeature(stateObj, SUPPORT_STOP))) || + ((state === "paused" || state === "idle") && + supportsFeature(stateObj, SUPPORT_PLAY)) || + (state === "on" && + (supportsFeature(stateObj, SUPPORT_PLAY) || + supportsFeature(stateObj, SUPPORT_PAUSE))) + ) { + buttons.push({ + icon: + state === "on" + ? "hass:play-pause" + : state !== "playing" + ? "hass:play" + : supportsFeature(stateObj, SUPPORT_PAUSE) + ? "hass:pause" + : "hass:stop", + action: + state === "playing" && !supportsFeature(stateObj, SUPPORT_PAUSE) + ? "media_stop" + : "media_play_pause", + }); + } + + if ( + (state === "playing" || state === "paused") && + supportsFeature(stateObj, SUPPORT_NEXT_TRACK) + ) { + buttons.push({ + icon: "hass:skip-next", + action: "media_next_track", + }); + } + + return buttons.length > 0 ? buttons : undefined; +}; diff --git a/src/data/panel.ts b/src/data/panel.ts index eb205c65b0..b71721c71c 100644 --- a/src/data/panel.ts +++ b/src/data/panel.ts @@ -21,6 +21,18 @@ export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => ? hass.panels[hass.defaultPanel] : hass.panels[DEFAULT_PANEL]; +export const getPanelNameTranslationKey = (panel: PanelInfo): string => { + if (panel.url_path === "lovelace") { + return "panel.states"; + } + + if (panel.url_path === "profile") { + return "panel.profile"; + } + + return `panel.${panel.title}`; +}; + export const getPanelTitle = (hass: HomeAssistant): string | undefined => { if (!hass.panels) { return undefined; @@ -34,13 +46,20 @@ export const getPanelTitle = (hass: HomeAssistant): string | undefined => { return undefined; } - if (panel.url_path === "lovelace") { - return hass.localize("panel.states"); - } + const translationKey = getPanelNameTranslationKey(panel); - if (panel.url_path === "profile") { - return hass.localize("panel.profile"); - } - - return hass.localize(`panel.${panel.title}`) || panel.title || undefined; + return hass.localize(translationKey) || panel.title || undefined; +}; + +export const getPanelIcon = (panel: PanelInfo): string | null => { + if (!panel.icon) { + switch (panel.component_name) { + case "profile": + return "hass:account"; + case "lovelace": + return "hass:view-dashboard"; + } + } + + return panel.icon; }; diff --git a/src/data/system_health.ts b/src/data/system_health.ts index fdf5007d10..72d5e20068 100644 --- a/src/data/system_health.ts +++ b/src/data/system_health.ts @@ -1,24 +1,111 @@ import { HomeAssistant } from "../types"; -export interface HomeAssistantSystemHealthInfo { - version: string; - dev: boolean; - hassio: boolean; - virtualenv: string; - python_version: string; - docker: boolean; - arch: string; - timezone: string; - os_name: string; +interface SystemCheckValueDateObject { + type: "date"; + value: string; } +interface SystemCheckValueErrorObject { + type: "failed"; + error: string; + more_info?: string; +} + +interface SystemCheckValuePendingObject { + type: "pending"; +} + +export type SystemCheckValueObject = + | SystemCheckValueDateObject + | SystemCheckValueErrorObject + | SystemCheckValuePendingObject; + +export type SystemCheckValue = + | string + | number + | boolean + | SystemCheckValueObject; + export interface SystemHealthInfo { - [domain: string]: { [key: string]: string | number | boolean }; + [domain: string]: { + manage_url?: string; + info: { + [key: string]: SystemCheckValue; + }; + }; } -export const fetchSystemHealthInfo = ( - hass: HomeAssistant -): Promise => - hass.callWS({ - type: "system_health/info", - }); +interface SystemHealthEventInitial { + type: "initial"; + data: SystemHealthInfo; +} +interface SystemHealthEventUpdateSuccess { + type: "update"; + success: true; + domain: string; + key: string; + data: SystemCheckValue; +} + +interface SystemHealthEventUpdateError { + type: "update"; + success: false; + domain: string; + key: string; + error: { + msg: string; + }; +} + +interface SystemHealthEventFinish { + type: "finish"; +} + +type SystemHealthEvent = + | SystemHealthEventInitial + | SystemHealthEventUpdateSuccess + | SystemHealthEventUpdateError + | SystemHealthEventFinish; + +export const subscribeSystemHealthInfo = ( + hass: HomeAssistant, + callback: (info: SystemHealthInfo) => void +) => { + let data = {}; + + const unsubProm = hass.connection.subscribeMessage( + (updateEvent) => { + if (updateEvent.type === "initial") { + data = updateEvent.data; + callback(data); + return; + } + if (updateEvent.type === "finish") { + unsubProm.then((unsub) => unsub()); + return; + } + + data = { + ...data, + [updateEvent.domain]: { + ...data[updateEvent.domain], + info: { + ...data[updateEvent.domain].info, + [updateEvent.key]: updateEvent.success + ? updateEvent.data + : { + error: true, + value: updateEvent.error.msg, + }, + }, + }, + }; + callback(data); + }, + { + type: "system_health/info", + } + ); + + return unsubProm; +}; diff --git a/src/data/tasmota.ts b/src/data/tasmota.ts new file mode 100644 index 0000000000..df9aad3032 --- /dev/null +++ b/src/data/tasmota.ts @@ -0,0 +1,10 @@ +import { HomeAssistant } from "../types"; + +export const removeTasmotaDeviceEntry = ( + hass: HomeAssistant, + deviceId: string +): Promise => + hass.callWS({ + type: "tasmota/device/remove", + device_id: deviceId, + }); diff --git a/src/data/translation.ts b/src/data/translation.ts index 8a11515458..4316242262 100644 --- a/src/data/translation.ts +++ b/src/data/translation.ts @@ -17,7 +17,8 @@ export type TranslationCategory = | "config" | "options" | "device_automation" - | "mfa_setup"; + | "mfa_setup" + | "system_health"; export const fetchTranslationPreferences = (hass: HomeAssistant) => fetchFrontendUserData(hass.connection, "language"); diff --git a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts index d7a2ad6fbc..54ab1c7f41 100644 --- a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts +++ b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts @@ -1,5 +1,4 @@ -import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; -import "@polymer/paper-input/paper-input"; +import "@material/mwc-button/mwc-button"; import { css, CSSResult, @@ -10,16 +9,16 @@ import { internalProperty, TemplateResult, } from "lit-element"; -import "../../components/dialog/ha-paper-dialog"; +import "../../components/ha-dialog"; import "../../components/ha-circular-progress"; import "../../components/ha-switch"; import "../../components/ha-formfield"; +import { fireEvent } from "../../common/dom/fire_event"; import type { HaSwitch } from "../../components/ha-switch"; import { getConfigEntrySystemOptions, updateConfigEntrySystemOptions, } from "../../data/config_entries"; -import type { PolymerChangedEvent } from "../../polymer-types"; import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; import { ConfigEntrySystemOptionsDialogParams } from "./show-dialog-config-entry-system-options"; @@ -35,9 +34,9 @@ class DialogConfigEntrySystemOptions extends LitElement { @internalProperty() private _params?: ConfigEntrySystemOptionsDialogParams; - @internalProperty() private _loading?: boolean; + @internalProperty() private _loading = false; - @internalProperty() private _submitting?: boolean; + @internalProperty() private _submitting = false; public async showDialog( params: ConfigEntrySystemOptionsDialogParams @@ -51,7 +50,12 @@ class DialogConfigEntrySystemOptions extends LitElement { ); this._loading = false; this._disableNewEntities = systemOptions.disable_new_entities; - await this.updateComplete; + } + + public closeDialog(): void { + this._error = ""; + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render(): TemplateResult { @@ -60,21 +64,17 @@ class DialogConfigEntrySystemOptions extends LitElement { } return html` - -

- ${this.hass.localize( - "ui.dialogs.config_entry_system_options.title", - "integration", - this.hass.localize( - `component.${this._params.entry.domain}.title` - ) || this._params.entry.domain - )} -

- +
${this._loading ? html`
@@ -112,22 +112,22 @@ class DialogConfigEntrySystemOptions extends LitElement {
`} - - ${!this._loading - ? html` -
- - ${this.hass.localize( - "ui.dialogs.config_entry_system_options.update" - )} - -
- ` - : ""} - +
+ + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize("ui.dialogs.config_entry_system_options.update")} + + `; } @@ -154,19 +154,10 @@ class DialogConfigEntrySystemOptions extends LitElement { } } - private _openedChanged(ev: PolymerChangedEvent): void { - if (!(ev.detail as any).value) { - this._params = undefined; - } - } - static get styles(): CSSResult[] { return [ haStyleDialog, css` - ha-paper-dialog { - max-width: 500px; - } .init-spinner { padding: 50px 100px; text-align: center; diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 79a95c2bc2..2b5660bf89 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -36,6 +36,7 @@ import "./step-flow-external"; import "./step-flow-form"; import "./step-flow-loading"; import "./step-flow-pick-handler"; +import "./step-flow-progress"; let instance = 0; @@ -195,6 +196,14 @@ class DataEntryFlowDialog extends LitElement { .hass=${this.hass} > ` + : this._step.type === "progress" + ? html` + + ` : this._devices === undefined || this._areas === undefined ? // When it's a create entry result, we will fetch device & area registry html` ` diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index 92bbb21597..a42663ea6b 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -160,4 +160,21 @@ export const showConfigFlowDialog = (

`; }, + + renderShowFormProgressHeader(hass, step) { + return hass.localize(`component.${step.handler}.title`); + }, + + renderShowFormProgressDescription(hass, step) { + const description = localizeKey( + hass.localize, + `component.${step.handler}.config.progress.${step.progress_action}`, + step.description_placeholders + ); + return description + ? html` + + ` + : ""; + }, }); diff --git a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts index 401c2a43e7..8c18bfb282 100644 --- a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts @@ -7,6 +7,7 @@ import { DataEntryFlowStepCreateEntry, DataEntryFlowStepExternal, DataEntryFlowStepForm, + DataEntryFlowStepProgress, } from "../../data/data_entry_flow"; import { HomeAssistant } from "../../types"; @@ -68,6 +69,16 @@ export interface FlowConfig { hass: HomeAssistant, step: DataEntryFlowStepCreateEntry ): TemplateResult | ""; + + renderShowFormProgressHeader( + hass: HomeAssistant, + step: DataEntryFlowStepProgress + ): string; + + renderShowFormProgressDescription( + hass: HomeAssistant, + step: DataEntryFlowStepProgress + ): TemplateResult | ""; } export interface DataEntryFlowDialogParams { diff --git a/src/dialogs/config-flow/show-dialog-options-flow.ts b/src/dialogs/config-flow/show-dialog-options-flow.ts index c3f228e568..1113b939a6 100644 --- a/src/dialogs/config-flow/show-dialog-options-flow.ts +++ b/src/dialogs/config-flow/show-dialog-options-flow.ts @@ -110,5 +110,13 @@ export const showOptionsFlowDialog = (

${hass.localize(`ui.dialogs.options_flow.success.description`)}

`; }, + + renderShowFormProgressHeader(_hass, _step) { + return ""; + }, + + renderShowFormProgressDescription(_hass, _step) { + return ""; + }, } ); diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index cbfcf3b8d5..a95f31637b 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -23,6 +23,7 @@ import { HomeAssistant } from "../../types"; import { documentationUrl } from "../../util/documentation-url"; import { FlowConfig } from "./show-dialog-data-entry-flow"; import { configFlowContentStyles } from "./styles"; +import { brandsUrl } from "../../util/brands-url"; interface HandlerObj { name: string; @@ -102,7 +103,7 @@ class StepFlowPickHandler extends LitElement { diff --git a/src/dialogs/config-flow/step-flow-progress.ts b/src/dialogs/config-flow/step-flow-progress.ts new file mode 100644 index 0000000000..57ba59560c --- /dev/null +++ b/src/dialogs/config-flow/step-flow-progress.ts @@ -0,0 +1,82 @@ +import "@material/mwc-button"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-circular-progress"; +import { + DataEntryFlowProgressedEvent, + DataEntryFlowStepProgress, +} from "../../data/data_entry_flow"; +import { HomeAssistant } from "../../types"; +import { FlowConfig } from "./show-dialog-data-entry-flow"; +import { configFlowContentStyles } from "./styles"; + +@customElement("step-flow-progress") +class StepFlowProgress extends LitElement { + public flowConfig!: FlowConfig; + + @property({ attribute: false }) + public hass!: HomeAssistant; + + @property({ attribute: false }) + private step!: DataEntryFlowStepProgress; + + protected render(): TemplateResult { + return html` +

+ ${this.flowConfig.renderShowFormProgressHeader(this.hass, this.step)} +

+
+ + ${this.flowConfig.renderShowFormProgressDescription( + this.hass, + this.step + )} +
+ `; + } + + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + this.hass.connection.subscribeEvents( + async (ev) => { + if (ev.data.flow_id !== this.step.flow_id) { + return; + } + + fireEvent(this, "flow-update", { + stepPromise: this.flowConfig.fetchFlow(this.hass, this.step.flow_id), + }); + }, + "data_entry_flow_progressed" + ); + } + + static get styles(): CSSResult[] { + return [ + configFlowContentStyles, + css` + .content { + padding: 50px 100px; + text-align: center; + } + ha-circular-progress { + margin-bottom: 16px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "step-flow-progress": StepFlowProgress; + } +} diff --git a/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts b/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts index f949d810db..4bc68af3a2 100644 --- a/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts +++ b/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts @@ -1,26 +1,28 @@ import "@material/mwc-button/mwc-button"; -import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-input/paper-input"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; +import "../../components/ha-dialog"; +import "../../components/ha-area-picker"; + import { - css, CSSResult, + LitElement, + TemplateResult, + css, customElement, html, - LitElement, - property, internalProperty, - TemplateResult, + property, } from "lit-element"; -import "../../components/dialog/ha-paper-dialog"; -import "../../components/ha-area-picker"; -import { computeDeviceName } from "../../data/device_registry"; -import { PolymerChangedEvent } from "../../polymer-types"; -import { haStyleDialog } from "../../resources/styles"; -import { HomeAssistant } from "../../types"; + import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail"; +import { HomeAssistant } from "../../types"; +import { PolymerChangedEvent } from "../../polymer-types"; +import { computeDeviceName } from "../../data/device_registry"; +import { fireEvent } from "../../common/dom/fire_event"; +import { haStyleDialog } from "../../resources/styles"; @customElement("dialog-device-registry-detail") class DialogDeviceRegistryDetail extends LitElement { @@ -34,7 +36,7 @@ class DialogDeviceRegistryDetail extends LitElement { @internalProperty() private _areaId?: string; - private _submitting?: boolean; + @internalProperty() private _submitting?: boolean; public async showDialog( params: DeviceRegistryDetailDialogParams @@ -46,22 +48,24 @@ class DialogDeviceRegistryDetail extends LitElement { await this.updateComplete; } + public closeDialog(): void { + this._error = ""; + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + protected render(): TemplateResult { if (!this._params) { return html``; } const device = this._params.device; - return html` - -

- ${computeDeviceName(device, this.hass)} -

- +
${this._error ? html`
${this._error}
` : ""}
- -
- - ${this.hass.localize("ui.panel.config.devices.update")} -
- + + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize("ui.panel.config.devices.update")} + + `; } @@ -113,19 +126,10 @@ class DialogDeviceRegistryDetail extends LitElement { } } - private _openedChanged(ev: PolymerChangedEvent): void { - if (!(ev.detail as any).value) { - this._params = undefined; - } - } - static get styles(): CSSResult[] { return [ haStyleDialog, css` - ha-paper-dialog { - min-width: 400px; - } .form { padding-bottom: 24px; } diff --git a/src/dialogs/more-info/controls/more-info-cover.js b/src/dialogs/more-info/controls/more-info-cover.js index d2d0d8770e..4e26d57e86 100644 --- a/src/dialogs/more-info/controls/more-info-cover.js +++ b/src/dialogs/more-info/controls/more-info-cover.js @@ -64,6 +64,10 @@ class MoreInfoCover extends LocalizeMixin(PolymerElement) {
+ `; } diff --git a/src/dialogs/more-info/controls/more-info-input_datetime.js b/src/dialogs/more-info/controls/more-info-input_datetime.js index f00a362820..b2e8c4453e 100644 --- a/src/dialogs/more-info/controls/more-info-input_datetime.js +++ b/src/dialogs/more-info/controls/more-info-input_datetime.js @@ -3,7 +3,7 @@ import "@polymer/paper-input/paper-input"; import { html } from "@polymer/polymer/lib/utils/html-tag"; /* eslint-plugin-disable lit */ import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker"; +import "../../../components/ha-date-input"; import { attributeClassNames } from "../../../common/entity/attribute_class_names"; import "../../../components/ha-relative-time"; import "../../../components/paper-time-input"; @@ -14,12 +14,12 @@ class DatetimeInput extends PolymerElement {