Merge pull request #7654 from home-assistant/dev

This commit is contained in:
Bram Kragten 2020-11-11 15:40:04 +01:00 committed by GitHub
commit 7e6153ba7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
243 changed files with 10135 additions and 3502 deletions

View File

@ -18,8 +18,8 @@
<!--
Describe the big picture of your changes here to communicate to the
maintainers why we should accept this pull request. If it fixes a bug
or resolves a feature request, be sure to link to that issue in the
additional information section.
or resolves a feature request, be sure to link to that issue or discussion
in the additional information section.
-->
## 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

27
.github/lock.yml vendored
View File

@ -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

56
.github/stale.yml vendored
View File

@ -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

20
.github/workflows/lock.yml vendored Normal file
View File

@ -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: ""

42
.github/workflows/stale.yml vendored Normal file
View File

@ -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.

View File

@ -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") {

View File

@ -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",
},
],
},

View File

@ -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 () => {};
});
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@ -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`
<style>
#container {
min-height: calc(100vh - 128px);
background: var(--primary-background-color);
}
.cards {
display: flex;
flex-wrap: wrap;
@ -24,6 +29,9 @@ class DemoCards extends PolymerElement {
.filters {
margin-left: 60px;
}
ha-formfield {
margin-right: 16px;
}
</style>
<app-toolbar>
<div class="filters">
@ -31,16 +39,21 @@ class DemoCards extends PolymerElement {
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
</ha-switch>
</ha-formfield>
<ha-formfield label="Dark theme">
<ha-switch on-change="_darkThemeToggled"> </ha-switch>
</ha-formfield>
</div>
</app-toolbar>
<div class="cards">
<template is="dom-repeat" items="[[configs]]">
<demo-card
config="[[item]]"
show-config="[[_showConfig]]"
hass="[[hass]]"
></demo-card>
</template>
<div id="container">
<div class="cards">
<template is="dom-repeat" items="[[configs]]">
<demo-card
config="[[item]]"
show-config="[[_showConfig]]"
hass="[[hass]]"
></demo-card>
</template>
</div>
</div>
`;
}
@ -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);

View File

@ -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 {

View File

@ -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);

View File

@ -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,
}),
];

View File

@ -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);
}
}

View File

@ -98,4 +98,4 @@ class DemoButtonEntity extends PolymerElement {
}
}
customElements.define("demo-hui-button-card", DemoButtonEntity);
customElements.define("demo-hui-entity-button-card", DemoButtonEntity);

View File

@ -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 {

View File

@ -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` <demo-cards configs="[[_configs]]"></demo-cards> `;
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
}
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);

View File

@ -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

View File

@ -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)
`,
},
];

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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` <hass-loading-screen></hass-loading-screen> `;
@ -44,6 +58,7 @@ class HassioIngressView extends LitElement {
if (!this.ingressPanel) {
return html`<hass-subpage
.hass=${this.hass}
.header=${this._addon.name}
.narrow=${this.narrow}
>
@ -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;
}

View File

@ -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",

View File

@ -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"
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -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",

View File

@ -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),

24
src/common/color/hex.ts Normal file
View File

@ -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}`;
};

View File

@ -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);

View File

@ -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}`;

View File

@ -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);

View File

@ -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";

View File

@ -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();
};

View File

@ -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.

View File

@ -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);
};

View File

@ -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`
<ha-icon-button
aria-label=${this.hass.localize(
<div class="suffix" slot="suffix">
${this.value
? html`<mwc-icon-button
class="clear-button"
.label=${this.hass.localize(
"ui.components.device-picker.clear"
)}
slot="suffix"
class="clear-button"
icon="hass:close"
@click=${this._clearValue}
no-ripple
>
Clear
</ha-icon-button>
`
: ""}
${areas.length > 0
? html`
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.device-picker.show_devices"
)}
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</ha-icon-button>
`
: ""}
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button> `
: ""}
${areas.length > 0
? html`
<mwc-icon-button
.label=${this.hass.localize(
"ui.components.device-picker.show_devices"
)}
class="toggle-button"
>
<ha-svg-icon
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
`
: ""}
</div>
</paper-input>
</vaadin-combo-box-light>
<mwc-button @click=${this._switchPicker}
@ -409,10 +409,12 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
static get styles(): CSSResult {
return css`
paper-input > 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] {

View File

@ -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;

View File

@ -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 }) => {
<paper-item></paper-item>
`;
}
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<boolean>) {

View File

@ -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` <ha-icon icon="[[computeIcon(stateObj)]]"></ha-icon> `;
}
static get properties() {
return {
stateObj: {
type: Object,
},
};
}
computeIcon(stateObj) {
return stateIcon(stateObj);
}
}
customElements.define("ha-state-icon", HaStateIcon);

View File

@ -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;
}
</style>
`;
}
@ -81,11 +94,26 @@ class StateInfo extends LocalizeMixin(PolymerElement) {
datetime="[[stateObj.last_changed]]"
></ha-relative-time>
<paper-tooltip animation-delay="0" for="last_changed">
[[localize('ui.dialogs.more_info_control.last_updated')]]:
<ha-relative-time
hass="[[hass]]"
datetime="[[stateObj.last_updated]]"
></ha-relative-time>
<div>
<div class="row">
<span class="column-name">
[[localize('ui.dialogs.more_info_control.last_changed')]]:
</span>
<ha-relative-time
hass="[[hass]]"
datetime="[[stateObj.last_changed]]"
></ha-relative-time>
</div>
<div class="row">
<span>
[[localize('ui.dialogs.more_info_control.last_updated')]]:
</span>
<ha-relative-time
hass="[[hass]]"
datetime="[[stateObj.last_updated]]"
></ha-relative-time>
</div>
</div>
</paper-tooltip>
</div>
</template>

View File

@ -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<typeof import("js-yaml")>;
@ -34,7 +36,7 @@ class HaAttributes extends LitElement {
(attribute) => html`
<div class="data-entry">
<div class="key">
${attribute.replace(/_/g, " ").replace(/\bid\b/g, "ID")}
${formatAttributeName(attribute)}
</div>
<div class="value">
${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;

View File

@ -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":

View File

@ -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`
<style>
: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;
}
</style>
<div class="target">
<template is="dom-if" if="[[_hasKnownState(stateObj.state)]]">
<span class="state-label">
[[_localizeState(localize, stateObj)]]
<template is="dom-if" if="[[_renderPreset(stateObj.attributes)]]">
- [[_localizePreset(localize, stateObj.attributes.preset_mode)]]
</template>
</span>
</template>
<div class="unit">[[computeTarget(hass, stateObj)]]</div>
</div>
<template is="dom-if" if="[[currentStatus]]">
<div class="current">
[[localize('ui.card.climate.currently')]]:
<div class="unit">[[currentStatus]]</div>
</div>
</template>
`;
}
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);

View File

@ -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`<div class="target">
${this.stateObj.state !== "unknown"
? html`<span class="state-label">
${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}`
: ""}
</span>`
: ""}
<div class="unit">${this._computeTarget()}</div>
</div>
${currentStatus
? html`<div class="current">
${this.hass.localize("ui.card.climate.currently")}:
<div class="unit">${currentStatus}</div>
</div>`
: ""}`;
}
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;
}
}

View File

@ -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`
<style include="iron-flex"></style>
<style>
:host {
white-space: nowrap;
}
[invisible] {
visibility: hidden !important;
}
</style>
<ha-icon-button
aria-label="Open cover tilt"
icon="hass:arrow-top-right"
on-click="onOpenTiltTap"
title="Open tilt"
invisible$="[[!entityObj.supportsOpenTilt]]"
disabled="[[computeOpenDisabled(stateObj, entityObj)]]"
></ha-icon-button>
<ha-icon-button
aria-label="Stop cover from moving"
icon="hass:stop"
on-click="onStopTiltTap"
invisible$="[[!entityObj.supportsStopTilt]]"
disabled="[[computeStopDisabled(stateObj)]]"
title="Stop tilt"
></ha-icon-button>
<ha-icon-button
aria-label="Close cover tilt"
icon="hass:arrow-bottom-left"
on-click="onCloseTiltTap"
title="Close tilt"
invisible$="[[!entityObj.supportsCloseTilt]]"
disabled="[[computeClosedDisabled(stateObj, entityObj)]]"
></ha-icon-button>
`;
}
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);

View File

@ -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` <ha-icon-button
class=${classMap({
invisible: !this._entityObj.supportsStop,
})}
label=${this.hass.localize(
"ui.dialogs.more_info_control.open_tilt_cover"
)}
icon="hass:arrow-top-right"
@click=${this._onOpenTiltTap}
.disabled=${this._computeOpenDisabled()}
></ha-icon-button>
<ha-icon-button
class=${classMap({
invisible: !this._entityObj.supportsStop,
})}
label=${this.hass.localize("ui.dialogs.more_info_control.stop_cover")}
icon="hass:stop"
@click=${this._onStopTiltTap}
.disabled=${this.stateObj.state === UNAVAILABLE}
></ha-icon-button>
<ha-icon-button
class=${classMap({
invisible: !this._entityObj.supportsStop,
})}
label=${this.hass.localize(
"ui.dialogs.more_info_control.open_tilt_cover"
)}
icon="hass:arrow-bottom-left"
@click=${this._onCloseTiltTap}
.disabled=${this._computeClosedDisabled()}
></ha-icon-button>`;
}
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;
}
}

View File

@ -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`
<div class="date-input-wrap">
<paper-input
id="year"
type="number"
.value=${this.year}
@change=${this._formatYear}
maxlength="4"
max="9999"
min="0"
.disabled=${this.disabled}
no-label-float
>
<span suffix="" slot="suffix">-</span>
</paper-input>
<paper-input
id="month"
type="number"
.value=${this.month}
@change=${this._formatMonth}
maxlength="2"
max="12"
min="1"
.disabled=${this.disabled}
no-label-float
>
<span suffix="" slot="suffix">-</span>
</paper-input>
<paper-input
id="day"
type="number"
.value=${this.day}
@change=${this._formatDay}
maxlength="2"
max="31"
min="1"
.disabled=${this.disabled}
no-label-float
>
</paper-input>
</div>
`;
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;

View File

@ -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);

View File

@ -104,7 +104,6 @@ class HaHLSPlayer extends LitElement {
private async _startHls(): Promise<void> {
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=".*?(?<isHevc>hev1|hvc1)?\..*?".*?)?(?:\n|\r\n)(?<streamUrl>.+)/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);

View File

@ -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`<mwc-icon-button .disabled=${this.disabled}>
return html`<mwc-icon-button
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `;
}

View File

@ -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`<mwc-icon-button .disabled=${this.disabled}>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `;
return html`
<mwc-icon-button
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button>
`;
}
}

View File

@ -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`<mwc-icon-button .disabled=${this.disabled}>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `;
return html`
<mwc-icon-button
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button>
`;
}
}

View File

@ -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`<mwc-icon-button .disabled=${this.disabled}>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `;
return html`
<mwc-icon-button
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button>
`;
}
}

View File

@ -14,7 +14,7 @@ class HaLabeledSlider extends PolymerElement {
}
.title {
margin-bottom: 8px;
margin: 4px 0 8px;
color: var(--primary-text-color);
}

View File

@ -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/)

View File

@ -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;

View File

@ -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`
<div
class="menu"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: !this.editMode,
disabled: this.editMode,
})}
>
${!this.narrow
? html`
<mwc-icon-button
.label=${hass.localize("ui.sidebar.sidebar_toggle")}
@action=${this._toggleSidebar}
>
<ha-svg-icon
.path=${hass.dockedSidebar === "docked"
? mdiMenuOpen
: mdiMenu}
></ha-svg-icon>
</mwc-icon-button>
`
: ""}
<div class="title">
${this.editMode
? html`<mwc-button outlined @click=${this._closeEditMode}>
${hass.localize("ui.sidebar.done")}
</mwc-button>`
: "Home Assistant"}
</div>
</div>
<paper-listbox
attr-for-selected="data-panel"
class="ha-scrollbar"
.selected=${hass.panelUrl}
@focusin=${this._listboxFocusIn}
@focusout=${this._listboxFocusOut}
@scroll=${this._listboxScroll}
@keydown=${this._listboxKeydown}
>
${this.editMode
? html`<div id="sortable">
${guard([this._hiddenPanels, this._renderEmptySortable], () =>
this._renderEmptySortable
? ""
: this._renderPanels(beforeSpacer)
)}
</div>`
: this._renderPanels(beforeSpacer)}
<div class="spacer" disabled></div>
${this.editMode && this._hiddenPanels.length
? html`
${this._hiddenPanels.map((url) => {
const panel = this.hass.panels[url];
if (!panel) {
return "";
}
return html`<paper-icon-item
@click=${this._unhidePanel}
class="hidden-panel"
.panel=${url}
>
<ha-icon
slot="item-icon"
.icon=${panel.url_path === this.hass.defaultPanel
? "mdi:view-dashboard"
: panel.icon}
></ha-icon>
<span class="item-text"
>${panel.url_path === this.hass.defaultPanel
? hass.localize("panel.states")
: hass.localize(`panel.${panel.title}`) ||
panel.title}</span
>
<mwc-icon-button class="show-panel">
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
</mwc-icon-button>
</paper-icon-item>`;
})}
<div class="spacer" disabled></div>
`
: ""}
${this._renderPanels(afterSpacer)}
${this._externalConfig && this._externalConfig.hasSettingsScreen
? html`
<a
aria-role="option"
aria-label=${hass.localize(
"ui.sidebar.external_app_configuration"
)}
href="#external-app-configuration"
tabindex="-1"
@click=${this._handleExternalAppConfiguration}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
<ha-svg-icon
slot="item-icon"
.path=${mdiCellphoneCog}
></ha-svg-icon>
<span class="item-text">
${hass.localize("ui.sidebar.external_app_configuration")}
</span>
</paper-icon-item>
</a>
`
: ""}
</paper-listbox>
<div class="divider"></div>
<div
class="notifications-container"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item
class="notifications"
aria-role="option"
@click=${this._handleShowNotificationDrawer}
>
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
${!this.expanded && notificationCount > 0
? html`
<span class="notification-badge" slot="item-icon">
${notificationCount}
</span>
`
: ""}
<span class="item-text">
${hass.localize("ui.notification_drawer.title")}
</span>
${this.expanded && notificationCount > 0
? html`
<span class="notification-badge">${notificationCount}</span>
`
: ""}
</paper-icon-item>
</div>
<a
class=${classMap({
profile: true,
// Mimick behavior that paper-listbox provides
"iron-selected": hass.panelUrl === "profile",
})}
href="/profile"
data-panel="panel"
tabindex="-1"
aria-role="option"
aria-label=${hass.localize("panel.profile")}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
<ha-user-badge
slot="item-icon"
.user=${hass.user}
.hass=${hass}
></ha-user-badge>
<span class="item-text">
${hass.user ? hass.user.name : ""}
</span>
</paper-icon-item>
</a>
${this._renderHeader()}
${this._renderAllPanels()}
${this._renderDivider()}
${this._renderNotifications()}
${this._renderUserItem()}
<div disabled class="bottom-spacer"></div>
<div class="tooltip"></div>
`;
@ -475,6 +297,213 @@ class HaSidebar extends LitElement {
}
}
private _renderHeader() {
return html`<div
class="menu"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: !this.editMode,
disabled: this.editMode,
})}
>
${!this.narrow
? html`
<mwc-icon-button
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
@action=${this._toggleSidebar}
>
<ha-svg-icon
.path=${this.hass.dockedSidebar === "docked"
? mdiMenuOpen
: mdiMenu}
></ha-svg-icon>
</mwc-icon-button>
`
: ""}
${this.editMode
? html`<mwc-button outlined @click=${this._closeEditMode}>
${this.hass.localize("ui.sidebar.done")}
</mwc-button>`
: html`<div class="title">Home Assistant</div>`}
</div>`;
}
private _renderAllPanels() {
const [beforeSpacer, afterSpacer] = computePanels(
this.hass.panels,
this.hass.defaultPanel,
this._panelOrder,
this._hiddenPanels
);
// prettier-ignore
return html`
<paper-listbox
attr-for-selected="data-panel"
class="ha-scrollbar"
.selected=${this.hass.panelUrl}
@focusin=${this._listboxFocusIn}
@focusout=${this._listboxFocusOut}
@scroll=${this._listboxScroll}
@keydown=${this._listboxKeydown}
>
${this.editMode
? this._renderPanelsEdit(beforeSpacer)
: this._renderPanels(beforeSpacer)}
${this._renderSpacer()}
${this._renderPanels(afterSpacer)}
${this._renderExternalConfiguration()}
</paper-listbox>
`;
}
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
// prettier-ignore
return html`<div id="sortable">
${guard([this._hiddenPanels, this._renderEmptySortable], () =>
this._renderEmptySortable ? "" : this._renderPanels(beforeSpacer)
)}
</div>
${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`<paper-icon-item
@click=${this._unhidePanel}
class="hidden-panel"
.panel=${url}
>
<ha-icon
slot="item-icon"
.icon=${panel.url_path === this.hass.defaultPanel
? "mdi:view-dashboard"
: panel.icon}
></ha-icon>
<span class="item-text"
>${panel.url_path === this.hass.defaultPanel
? this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) ||
panel.title}</span
>
<mwc-icon-button class="show-panel">
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
</mwc-icon-button>
</paper-icon-item>`;
})}
${this._renderSpacer()}`
: ""}`;
}
private _renderDivider() {
return html`<div class="divider"></div>`;
}
private _renderSpacer() {
return html`<div class="spacer" disabled></div>`;
}
private _renderNotifications() {
let notificationCount = this._notifications
? this._notifications.length
: 0;
for (const entityId in this.hass.states) {
if (computeDomain(entityId) === "configurator") {
notificationCount++;
}
}
return html` <div
class="notifications-container"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item
class="notifications"
aria-role="option"
@click=${this._handleShowNotificationDrawer}
>
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
${!this.expanded && notificationCount > 0
? html`
<span class="notification-badge" slot="item-icon">
${notificationCount}
</span>
`
: ""}
<span class="item-text">
${this.hass.localize("ui.notification_drawer.title")}
</span>
${this.expanded && notificationCount > 0
? html` <span class="notification-badge">${notificationCount}</span> `
: ""}
</paper-icon-item>
</div>`;
}
private _renderUserItem() {
return html`<a
class=${classMap({
profile: true,
// Mimick behavior that paper-listbox provides
"iron-selected": this.hass.panelUrl === "profile",
})}
href="/profile"
data-panel="panel"
tabindex="-1"
aria-role="option"
aria-label=${this.hass.localize("panel.profile")}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
<ha-user-badge
slot="item-icon"
.user=${this.hass.user}
.hass=${this.hass}
></ha-user-badge>
<span class="item-text">
${this.hass.user ? this.hass.user.name : ""}
</span>
</paper-icon-item>
</a>`;
}
private _renderExternalConfiguration() {
return html`${this._externalConfig && this._externalConfig.hasSettingsScreen
? html`
<a
aria-role="option"
aria-label=${this.hass.localize(
"ui.sidebar.external_app_configuration"
)}
href="#external-app-configuration"
tabindex="-1"
@click=${this._handleExternalAppConfiguration}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
<ha-svg-icon
slot="item-icon"
.path=${mdiCellphoneCog}
></ha-svg-icon>
<span class="item-text">
${this.hass.localize("ui.sidebar.external_app_configuration")}
</span>
</paper-icon-item>
</a>
`
: ""}`;
}
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;

View File

@ -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 {

View File

@ -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;

View File

@ -540,17 +540,20 @@ export class HaMediaPlayerBrowse extends LitElement {
mediaContentType?: string
): Promise<MediaPlayerItem> {
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;
}

View File

@ -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;

View File

@ -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<EntityRegistryEntryUpdateParams>
): Promise<ExtEntityRegistryEntry> =>
): Promise<UpdateEntityRegistryEntryResult> =>
hass.callWS({
type: "config/entity_registry/update",
entity_id: entityId,

View File

@ -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<HassioResponse<CreateSessionResponse>>(
"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<HassioResponse<null>>(
"POST",
"hassio/ingress/validate_session",
{ session }
);

View File

@ -111,18 +111,6 @@ export const fetchHassioLogs = async (
return hass.callApi<string>("GET", `hassio/${provider}/logs`);
};
export const createHassioSession = async (hass: HomeAssistant) => {
const response = await hass.callApi<HassioResponse<CreateSessionResponse>>(
"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

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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<SystemHealthInfo> =>
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<SystemHealthEvent>(
(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;
};

10
src/data/tasmota.ts Normal file
View File

@ -0,0 +1,10 @@
import { HomeAssistant } from "../types";
export const removeTasmotaDeviceEntry = (
hass: HomeAssistant,
deviceId: string
): Promise<void> =>
hass.callWS({
type: "tasmota/device/remove",
device_id: deviceId,
});

View File

@ -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");

View File

@ -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`
<ha-paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${this.hass.localize(
"ui.dialogs.config_entry_system_options.title",
"integration",
this.hass.localize(`component.${this._params.entry.domain}.title`) ||
this._params.entry.domain
)}
>
<h2>
${this.hass.localize(
"ui.dialogs.config_entry_system_options.title",
"integration",
this.hass.localize(
`component.${this._params.entry.domain}.title`
) || this._params.entry.domain
)}
</h2>
<paper-dialog-scrollable>
<div>
${this._loading
? html`
<div class="init-spinner">
@ -112,22 +112,22 @@ class DialogConfigEntrySystemOptions extends LitElement {
</ha-formfield>
</div>
`}
</paper-dialog-scrollable>
${!this._loading
? html`
<div class="paper-dialog-buttons">
<mwc-button
@click="${this._updateEntry}"
.disabled=${this._submitting}
>
${this.hass.localize(
"ui.dialogs.config_entry_system_options.update"
)}
</mwc-button>
</div>
`
: ""}
</ha-paper-dialog>
</div>
<mwc-button
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
>
${this.hass.localize("ui.common.cancel")}
</mwc-button>
<mwc-button
slot="primaryAction"
@click="${this._updateEntry}"
.disabled=${this._submitting || this._loading}
>
${this.hass.localize("ui.dialogs.config_entry_system_options.update")}
</mwc-button>
</ha-dialog>
`;
}
@ -154,19 +154,10 @@ class DialogConfigEntrySystemOptions extends LitElement {
}
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): 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;

View File

@ -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}
></step-flow-abort>
`
: this._step.type === "progress"
? html`
<step-flow-progress
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-progress>
`
: this._devices === undefined || this._areas === undefined
? // When it's a create entry result, we will fetch device & area registry
html` <step-flow-loading></step-flow-loading> `

View File

@ -160,4 +160,21 @@ export const showConfigFlowDialog = (
</p>
`;
},
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`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
`
: "";
},
});

View File

@ -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 {

View File

@ -110,5 +110,13 @@ export const showOptionsFlowDialog = (
<p>${hass.localize(`ui.dialogs.options_flow.success.description`)}</p>
`;
},
renderShowFormProgressHeader(_hass, _step) {
return "";
},
renderShowFormProgressDescription(_hass, _step) {
return "";
},
}
);

View File

@ -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 {
<img
slot="item-icon"
loading="lazy"
src="https://brands.home-assistant.io/_/${handler.slug}/icon.png"
src=${brandsUrl(handler.slug, "icon", true)}
referrerpolicy="no-referrer"
/>

View File

@ -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`
<h2>
${this.flowConfig.renderShowFormProgressHeader(this.hass, this.step)}
</h2>
<div class="content">
<ha-circular-progress active></ha-circular-progress>
${this.flowConfig.renderShowFormProgressDescription(
this.hass,
this.step
)}
</div>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.hass.connection.subscribeEvents<DataEntryFlowProgressedEvent>(
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;
}
}

View File

@ -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`
<ha-paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${computeDeviceName(device, this.hass)}
>
<h2>
${computeDeviceName(device, this.hass)}
</h2>
<paper-dialog-scrollable>
<div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<div class="form">
<paper-input
@ -77,13 +81,22 @@ class DialogDeviceRegistryDetail extends LitElement {
@value-changed=${this._areaPicked}
></ha-area-picker>
</div>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<mwc-button @click="${this._updateEntry}">
${this.hass.localize("ui.panel.config.devices.update")}
</mwc-button>
</div>
</ha-paper-dialog>
<mwc-button
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
>
${this.hass.localize("ui.common.cancel")}
</mwc-button>
<mwc-button
slot="primaryAction"
@click="${this._updateEntry}"
.disabled=${this._submitting}
>
${this.hass.localize("ui.panel.config.devices.update")}
</mwc-button>
</ha-dialog>
`;
}
@ -113,19 +126,10 @@ class DialogDeviceRegistryDetail extends LitElement {
}
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): 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;
}

View File

@ -64,6 +64,10 @@ class MoreInfoCover extends LocalizeMixin(PolymerElement) {
</ha-labeled-slider>
</div>
</div>
<ha-attributes
state-obj="[[stateObj]]"
extra-filters="current_position,current_tilt_position"
></ha-attributes>
`;
}

View File

@ -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 {
<div class$="[[computeClassNames(stateObj)]]">
<template is="dom-if" if="[[doesHaveDate(stateObj)]]" restamp="">
<div>
<vaadin-date-picker
<ha-date-input
id="dateInput"
on-value-changed="dateTimeChanged"
label="Date"
value="{{selectedDate}}"
></vaadin-date-picker>
></ha-date-input>
</div>
</template>
<template is="dom-if" if="[[doesHaveTime(stateObj)]]" restamp="">

View File

@ -25,19 +25,12 @@ import "../../../components/ha-svg-icon";
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity";
import {
ControlButton,
computeMediaControls,
MediaPickedEvent,
SUPPORTS_PLAY,
SUPPORT_BROWSE_MEDIA,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOUND_MODE,
SUPPORT_SELECT_SOURCE,
SUPPORT_STOP,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
SUPPORT_VOLUME_BUTTONS,
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
@ -57,8 +50,8 @@ class MoreInfoMediaPlayer extends LitElement {
return html``;
}
const controls = this._getControls();
const stateObj = this.stateObj;
const controls = computeMediaControls(stateObj);
return html`
${!controls
@ -254,84 +247,6 @@ class MoreInfoMediaPlayer extends LitElement {
`;
}
private _getControls(): ControlButton[] | undefined {
const stateObj = this.stateObj;
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;
}
if (state === "idle") {
return supportsFeature(stateObj, SUPPORTS_PLAY)
? [
{
icon: "hass:play",
action: "media_play",
},
]
: undefined;
}
const buttons: ControlButton[] = [];
if (supportsFeature(stateObj, SUPPORT_TURN_OFF)) {
buttons.push({
icon: "hass:power",
action: "turn_off",
});
}
if (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" && supportsFeature(stateObj, SUPPORTS_PLAY))
) {
buttons.push({
icon:
state !== "playing"
? "hass:play"
: supportsFeature(stateObj, SUPPORT_PAUSE)
? "hass:pause"
: "hass:stop",
action: "media_play_pause",
});
}
if (supportsFeature(stateObj, SUPPORT_NEXT_TRACK)) {
buttons.push({
icon: "hass:skip-next",
action: "media_next_track",
});
}
return buttons.length > 0 ? buttons : undefined;
}
private _handleClick(e: MouseEvent): void {
this.hass!.callService(
"media_player",

View File

@ -72,6 +72,12 @@ class MoreInfoSun extends LitElement {
flex-direction: row;
justify-content: space-between;
}
ha-relative-time {
display: inline-block;
}
ha-relative-time::first-letter {
text-transform: lowercase;
}
`;
}
}

View File

@ -17,14 +17,10 @@ import {
DOMAINS_MORE_INFO_NO_HISTORY,
DOMAINS_WITH_MORE_INFO,
} from "../../common/const";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import {
stateMoreInfoType,
importMoreInfoControl,
} from "./state_more_info_control";
import { navigate } from "../../common/navigate";
import "../../components/ha-dialog";
import "../../components/ha-header-bar";
@ -39,6 +35,7 @@ import "./ha-more-info-history";
import "./ha-more-info-logbook";
import "./controls/more-info-default";
import { CONTINUOUS_DOMAINS } from "../../data/logbook";
import "./more-info-content";
const DOMAINS_NO_INFO = ["camera", "configurator"];
/**
@ -63,8 +60,6 @@ export class MoreInfoDialog extends LitElement {
@internalProperty() private _entityId?: string | null;
@internalProperty() private _moreInfoType?: string;
@internalProperty() private _currTabIndex = 0;
public showDialog(params: MoreInfoDialogParams) {
@ -74,18 +69,6 @@ export class MoreInfoDialog extends LitElement {
return;
}
this.large = false;
const stateObj = this.hass.states[this._entityId];
if (!stateObj) {
return;
}
if (stateObj.attributes && "custom_ui_more_info" in stateObj.attributes) {
this._moreInfoType = stateObj.attributes.custom_ui_more_info;
} else {
const type = stateMoreInfoType(stateObj);
importMoreInfoControl(type);
this._moreInfoType = type === "hidden" ? undefined : `more-info-${type}`;
}
}
public closeDialog() {
@ -218,12 +201,10 @@ export class MoreInfoDialog extends LitElement {
.hass=${this.hass}
.entityId=${this._entityId}
></ha-more-info-logbook>`}
${this._moreInfoType
? dynamicElement(this._moreInfoType, {
hass: this.hass,
stateObj,
})
: ""}
<more-info-content
.stateObj=${stateObj}
.hass=${this.hass}
></more-info-content>
${stateObj.attributes.restored
? html`
<p>
@ -304,8 +285,8 @@ export class MoreInfoDialog extends LitElement {
text: this.hass.localize(
"ui.dialogs.more_info_control.restored.confirm_remove_text"
),
confirmText: this.hass.localize("ui.common.yes"),
dismissText: this.hass.localize("ui.common.no"),
confirmText: this.hass.localize("ui.common.remove"),
dismissText: this.hass.localize("ui.common.cancel"),
confirm: () => {
removeEntityRegistryEntry(this.hass, entityId);
},

View File

@ -0,0 +1,57 @@
import { HassEntity } from "home-assistant-js-websocket";
import { property, PropertyValues, UpdatingElement } from "lit-element";
import { HomeAssistant } from "../../types";
import dynamicContentUpdater from "../../common/dom/dynamic_content_updater";
import { stateMoreInfoType } from "./state_more_info_control";
import { importMoreInfoControl } from "../../panels/lovelace/custom-card-helpers";
class MoreInfoContent extends UpdatingElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public stateObj?: HassEntity;
private _detachedChild?: ChildNode;
// 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;
}
let moreInfoType: string | undefined;
if (stateObj.attributes && "custom_ui_more_info" in stateObj.attributes) {
moreInfoType = stateObj.attributes.custom_ui_more_info;
} else {
const type = stateMoreInfoType(stateObj);
importMoreInfoControl(type);
moreInfoType = type === "hidden" ? undefined : `more-info-${type}`;
}
if (!moreInfoType) {
return;
}
dynamicContentUpdater(this, moreInfoType.toUpperCase(), {
hass,
stateObj,
});
}
}
customElements.define("more-info-content", MoreInfoContent);

View File

@ -25,8 +25,8 @@ export class HuiNotificationDrawer extends EventsMixin(
color: var(--primary-text-color);
border-bottom: 1px solid var(--divider-color);
background-color: var(--primary-background-color);
min-height: 64px;
width: calc(100% - 32px);
height: var(--header-height);
box-sizing: border-box;
}
div[main-title] {
@ -63,7 +63,10 @@ export class HuiNotificationDrawer extends EventsMixin(
<app-drawer id="drawer" opened="{{open}}" disable-swipe align="start">
<app-toolbar>
<div main-title>[[localize('ui.notification_drawer.title')]]</div>
<ha-icon-button-prev on-click="_closeDrawer" aria-label$="[[localize('ui.notification_drawer.close')]]"></ha-icon-button-prev>
<ha-icon-button-prev hass="[[hass]]" on-click="_closeDrawer"
title="[[localize('ui.notification_drawer.close')]]"
label="[[localize('ui.notification_drawer.close')]]">
</ha-icon-button-prev>
</app-toolbar>
<div class="notifications">
<template is="dom-if" if="[[!_empty(notifications)]]">

View File

@ -41,19 +41,36 @@ import {
showConfirmationDialog,
} from "../generic/show-dialog-box";
import { QuickBarParams } from "./show-dialog-quick-bar";
import { navigate } from "../../common/navigate";
import { configSections } from "../../panels/config/ha-panel-config";
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { canShowPage } from "../../common/config/can_show_page";
import { getPanelIcon, getPanelNameTranslationKey } from "../../data/panel";
const DEFAULT_NAVIGATION_ICON = "hass:arrow-right-circle";
const DEFAULT_SERVER_ICON = "hass:server";
interface QuickBarItem extends ScorableTextItem {
icon: string;
icon?: string;
iconPath?: string;
action(data?: any): void;
}
interface QuickBarNavigationItem extends QuickBarItem {
path: string;
}
interface NavigationInfo extends PageNavigation {
text: string;
}
@customElement("ha-quick-bar")
export class QuickBar extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _commandItems: QuickBarItem[] = [];
@internalProperty() private _commandItems?: QuickBarItem[];
@internalProperty() private _entityItems: QuickBarItem[] = [];
@internalProperty() private _entityItems?: QuickBarItem[];
@internalProperty() private _items?: QuickBarItem[] = [];
@ -73,8 +90,7 @@ export class QuickBar extends LitElement {
public async showDialog(params: QuickBarParams) {
this._commandMode = params.commandMode || this._toggleIfAlreadyOpened();
this._commandItems = this._generateCommandItems();
this._entityItems = this._generateEntityItems();
this._initializeItemsIfNeeded();
this._opened = true;
}
@ -158,6 +174,14 @@ export class QuickBar extends LitElement {
`;
}
private _initializeItemsIfNeeded() {
if (this._commandMode) {
this._commandItems = this._commandItems || this._generateCommandItems();
} else {
this._entityItems = this._entityItems || this._generateEntityItems();
}
}
private _handleOpened() {
this._setFilteredItems();
this.updateComplete.then(() => {
@ -182,14 +206,20 @@ export class QuickBar extends LitElement {
.twoline=${Boolean(item.altText)}
.item=${item}
index=${ifDefined(index)}
hasMeta
graphic=${item.altText ? "avatar" : "icon"}
graphic="icon"
>
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
<span>${item.text}</span>
${item.iconPath
? html`<ha-svg-icon
.path=${item.iconPath}
slot="graphic"
></ha-svg-icon>`
: html`<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>`}
${item.text}
${item.altText
? html`
<span slot="secondary" class="secondary">${item.altText}</span>
<span slot="secondary" class="item-text secondary"
>${item.altText}</span
>
`
: null}
</mwc-list-item>
@ -252,6 +282,8 @@ export class QuickBar extends LitElement {
if (oldCommandMode !== this._commandMode) {
this._items = undefined;
this._focusSet = false;
this._initializeItemsIfNeeded();
}
}
@ -279,10 +311,22 @@ export class QuickBar extends LitElement {
}
}
private _generateEntityItems(): QuickBarItem[] {
return Object.keys(this.hass.states)
.map((entityId) => ({
text: computeStateName(this.hass.states[entityId]),
altText: entityId,
icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]),
action: () => fireEvent(this, "hass-more-info", { entityId }),
}))
.sort((a, b) => compare(a.text.toLowerCase(), b.text.toLowerCase()));
}
private _generateCommandItems(): QuickBarItem[] {
return [
...this._generateReloadCommands(),
...this._generateServerControlCommands(),
...this._generateNavigationCommands(),
].sort((a, b) => compare(a.text.toLowerCase(), b.text.toLowerCase()));
}
@ -315,7 +359,7 @@ export class QuickBar extends LitElement {
`ui.dialogs.quick-bar.commands.server_control.${action}`
)
),
icon: "hass:server",
icon: DEFAULT_SERVER_ICON,
action: () => this.hass.callService("homeassistant", action),
},
this.hass.localize("ui.dialogs.generic.ok")
@ -323,6 +367,79 @@ export class QuickBar extends LitElement {
);
}
private _generateNavigationCommands(): QuickBarItem[] {
const panelItems = this._generateNavigationPanelCommands();
const sectionItems = this._generateNavigationConfigSectionCommands();
return this._withNavigationActions([...panelItems, ...sectionItems]);
}
private _generateNavigationPanelCommands(): Omit<
QuickBarNavigationItem,
"action"
>[] {
return Object.keys(this.hass.panels).map((panelKey) => {
const panel = this.hass.panels[panelKey];
const translationKey = getPanelNameTranslationKey(panel);
const text = this.hass.localize(
"ui.dialogs.quick-bar.commands.navigation.navigate_to",
"panel",
this.hass.localize(translationKey) || panel.title || panel.url_path
);
return {
text,
icon: getPanelIcon(panel) || DEFAULT_NAVIGATION_ICON,
path: `/${panel.url_path}`,
};
});
}
private _generateNavigationConfigSectionCommands(): Partial<
QuickBarNavigationItem
>[] {
const items: NavigationInfo[] = [];
for (const sectionKey of Object.keys(configSections)) {
for (const page of configSections[sectionKey]) {
if (canShowPage(this.hass, page)) {
if (page.component) {
const info = this._getNavigationInfoFromConfig(page);
if (info) {
items.push(info);
}
}
}
}
}
return items;
}
private _getNavigationInfoFromConfig(
page: PageNavigation
): NavigationInfo | undefined {
if (page.component) {
const shortCaption = this.hass.localize(
`ui.dialogs.quick-bar.commands.navigation.${page.component}`
);
if (page.translationKey) {
const caption = this.hass.localize(
"ui.dialogs.quick-bar.commands.navigation.navigate_to_config",
"panel",
shortCaption
);
return { ...page, text: caption };
}
}
return undefined;
}
private _generateConfirmationCommand(
item: QuickBarItem,
confirmText: ConfirmationDialogParams["confirmText"]
@ -337,15 +454,13 @@ export class QuickBar extends LitElement {
};
}
private _generateEntityItems(): QuickBarItem[] {
return Object.keys(this.hass.states)
.map((entityId) => ({
text: computeStateName(this.hass.states[entityId]) || entityId,
altText: entityId,
icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]),
action: () => fireEvent(this, "hass-more-info", { entityId }),
}))
.sort((a, b) => compare(a.text.toLowerCase(), b.text.toLowerCase()));
private _withNavigationActions(items) {
return items.map(({ text, icon, iconPath, path }) => ({
text,
icon,
iconPath,
action: () => navigate(this, path),
}));
}
private _toggleIfAlreadyOpened() {
@ -387,8 +502,14 @@ export class QuickBar extends LitElement {
}
}
ha-icon,
ha-svg-icon {
margin-left: 20px;
}
ha-svg-icon.prefix {
margin: 8px;
color: var(--primary-text-color);
}
.uni-virtualizer-host {
@ -405,6 +526,7 @@ export class QuickBar extends LitElement {
mwc-list-item {
width: 100%;
text-transform: capitalize;
}
`,
];

View File

@ -53,19 +53,21 @@ export const provideHass = (
} = {};
const entities = {};
function updateTranslations(fragment: null | string, language?: string) {
async function updateTranslations(
fragment: null | string,
language?: string
) {
const lang = language || getLocalLanguage();
getTranslation(fragment, lang).then(async (translation) => {
const resources = {
[lang]: {
...(hass().resources && hass().resources[lang]),
...translation.data,
},
};
hass().updateHass({
resources,
localize: await computeLocalize(elements[0], lang, resources),
});
const translation = await getTranslation(fragment, lang);
const resources = {
[lang]: {
...(hass().resources && hass().resources[lang]),
...translation.data,
},
};
hass().updateHass({
resources,
localize: await computeLocalize(elements[0], lang, resources),
});
}

View File

@ -17,6 +17,8 @@
--primary-text-color: #e1e1e1;
--secondary-text-color: #9b9b9b;
--disabled-text-color: #6f6f6f;
--mdc-theme-surface: #1e1e1e;
--ha-card-background: #1e1e1e;
}
}
.content {

View File

@ -9,9 +9,12 @@ import {
TemplateResult,
} from "lit-element";
import "./hass-subpage";
import { HomeAssistant } from "../types";
@customElement("hass-error-screen")
class HassErrorScreen extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public toolbar = true;
@property() public error?: string;
@ -21,6 +24,7 @@ class HassErrorScreen extends LitElement {
${this.toolbar
? html`<div class="toolbar">
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
</div>`
@ -50,7 +54,7 @@ class HassErrorScreen extends LitElement {
display: flex;
align-items: center;
font-size: 20px;
height: 65px;
height: var(--header-height);
padding: 0 16px;
pointer-events: none;
background-color: var(--app-header-background-color);

View File

@ -39,6 +39,7 @@ class HassLoadingScreen extends LitElement {
`
: html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
`}
@ -66,7 +67,7 @@ class HassLoadingScreen extends LitElement {
display: flex;
align-items: center;
font-size: 20px;
height: 65px;
height: var(--header-height);
padding: 0 16px;
pointer-events: none;
background-color: var(--app-header-background-color);

View File

@ -12,17 +12,17 @@ import { classMap } from "lit-html/directives/class-map";
import "../components/ha-menu-button";
import "../components/ha-icon-button-arrow-prev";
import { restoreScroll } from "../common/decorators/restore-scroll";
import { HomeAssistant } from "../types";
@customElement("hass-subpage")
class HassSubpage extends LitElement {
@property()
public header?: string;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean })
public showBackButton = true;
@property() public header?: string;
@property({ type: Boolean })
public hassio = false;
@property({ type: Boolean }) public showBackButton = true;
@property({ type: Boolean }) public hassio = false;
// @ts-ignore
@restoreScroll(".content") private _savedScrollPos?: number;
@ -31,7 +31,7 @@ class HassSubpage extends LitElement {
return html`
<div class="toolbar">
<ha-icon-button-arrow-prev
aria-label="Back"
.hass=${this.hass}
@click=${this._backTapped}
class=${classMap({ hidden: !this.showBackButton })}
></ha-icon-button-arrow-prev>
@ -69,7 +69,7 @@ class HassSubpage extends LitElement {
display: flex;
align-items: center;
font-size: 20px;
height: 65px;
height: var(--header-height);
padding: 0 16px;
pointer-events: none;
background-color: var(--app-header-background-color);

View File

@ -145,7 +145,7 @@ class HassTabsSubpage extends LitElement {
`
: html`
<ha-icon-button-arrow-prev
aria-label="Back"
.hass=${this.hass}
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`}
@ -217,7 +217,7 @@ class HassTabsSubpage extends LitElement {
display: flex;
align-items: center;
font-size: 20px;
height: 65px;
height: var(--header-height);
background-color: var(--sidebar-background-color);
font-weight: 400;
color: var(--sidebar-text-color);

View File

@ -192,7 +192,7 @@ class HomeAssistantMain extends LitElement {
color: var(--primary-text-color);
/* remove the grey tap highlights in iOS on the fullscreen touch targets */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
--app-drawer-width: 64px;
--app-drawer-width: 56px;
}
:host([expanded]) {
--app-drawer-width: calc(256px + env(safe-area-inset-left));

View File

@ -8,6 +8,7 @@ import {
TemplateResult,
} from "lit-element";
import "../components/ha-icon";
import { brandsUrl } from "../util/brands-url";
@customElement("integration-badge")
class IntegrationBadge extends LitElement {
@ -23,7 +24,7 @@ class IntegrationBadge extends LitElement {
return html`
<div class="icon">
<img
src="https://brands.home-assistant.io/${this.domain}/icon.png"
src=${brandsUrl(this.domain, "icon")}
referrerpolicy="no-referrer"
/>
${this.badgeIcon

View File

@ -4,7 +4,6 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
@ -15,10 +14,6 @@ import { showSnapshotUploadDialog } from "../../hassio/src/dialogs/snapshot/show
import { navigate } from "../common/navigate";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-card";
import {
extractApiErrorMessage,
ignoredStatusCodes,
} from "../data/hassio/common";
import { makeDialogManager } from "../dialogs/make-dialog-manager";
import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
import { haStyle } from "../resources/styles";
@ -38,10 +33,6 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
@property({ type: Boolean }) public restoring = false;
@internalProperty() private _log = "";
@internalProperty() private _showFullLog = false;
protected render(): TemplateResult {
return this.restoring
? html`<ha-card
@ -49,22 +40,7 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
"ui.panel.page-onboarding.restore.in_progress"
)}
>
${this._showFullLog
? html`<hassio-ansi-to-html .content=${this._log}>
</hassio-ansi-to-html>`
: html`<onboarding-loading></onboarding-loading>
<hassio-ansi-to-html
class="logentry"
.content=${this._lastLogEntry(this._log)}
>
</hassio-ansi-to-html>`}
<div class="card-actions">
<mwc-button @click=${this._toggeFullLog}>
${this._showFullLog
? this.localize("ui.panel.page-onboarding.restore.hide_log")
: this.localize("ui.panel.page-onboarding.restore.show_log")}
</mwc-button>
</div>
<onboarding-loading></onboarding-loading>
</ha-card>`
: html`
<button class="link" @click=${this._uploadSnapshot}>
@ -73,33 +49,6 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
`;
}
private _toggeFullLog(): void {
this._showFullLog = !this._showFullLog;
}
private _filterLogs(logs: string): string {
// Filter out logs that is not relevant to show during the restore
return logs
.split("\n")
.filter(
(entry) =>
!entry.includes("/supervisor/logs") &&
!entry.includes("/supervisor/ping") &&
!entry.includes("DEBUG") &&
!entry.includes("TypeError: Failed to fetch")
)
.join("\n")
.replace(/\s[A-Z]+\s\(\w+\)\s\[[\w.]+\]/gi, "")
.replace(/\d{2}-\d{2}-\d{2}\s/gi, "");
}
private _lastLogEntry(logs: string): string {
return logs
.split("\n")
.slice(-2)[0]
.replace(/\d{2}:\d{2}:\d{2}\s/gi, "");
}
private _uploadSnapshot(): void {
showSnapshotUploadDialog(this, {
showSnapshot: (slug: string) => this._showSnapshotDialog(slug),
@ -110,42 +59,26 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
makeDialogManager(this, this.shadowRoot!);
setInterval(() => this._getLogs(), 1000);
setInterval(() => this._checkRestoreStatus(), 1000);
}
private async _getLogs(): Promise<void> {
private async _checkRestoreStatus(): Promise<void> {
if (this.restoring) {
try {
const response = await fetch("/api/hassio/supervisor/logs", {
const response = await fetch("/api/hassio/supervisor/info", {
method: "GET",
});
if (response.status === 401) {
// If we get a unauthorized response, the restore is done
this._restoreDone();
} else if (
response.status &&
!ignoredStatusCodes.has(response.status)
) {
// Handle error responses
this._log += this._filterLogs(extractApiErrorMessage(response));
}
const logs = await response.text();
this._log = this._filterLogs(logs);
if (this._log.match(/\d{2}:\d{2}:\d{2}\s.*Restore\s\w+\sdone/)) {
// The log indicates that the restore done, navigate the user back to base
this._restoreDone();
navigate(this, "/", true);
location.reload();
}
} catch (err) {
this._log += this._filterLogs(err.toString());
// We fully expected issues with fetching info untill restore is complete.
}
}
}
private _restoreDone(): void {
navigate(this, "/", true);
location.reload();
}
private _showSnapshotDialog(slug: string): void {
showHassioSnapshotDialog(this, {
slug,

View File

@ -2,6 +2,7 @@
import fullcalendarStyle from "@fullcalendar/common/main.css";
import type { CalendarOptions } from "@fullcalendar/core";
import { Calendar } from "@fullcalendar/core";
import allLocales from "@fullcalendar/core/locales-all";
import dayGridPlugin from "@fullcalendar/daygrid";
// @ts-ignore
import daygridStyle from "@fullcalendar/daygrid/main.css";
@ -44,6 +45,15 @@ declare global {
}
}
const getListWeekRange = (currentDate: Date): { start: Date; end: Date } => {
const startDate = new Date(currentDate.valueOf());
const endDate = new Date(currentDate.valueOf());
endDate.setDate(endDate.getDate() + 7);
return { start: startDate, end: endDate };
};
const defaultFullCalendarConfig: CalendarOptions = {
headerToolbar: false,
plugins: [dayGridPlugin, listPlugin, interactionPlugin],
@ -51,16 +61,22 @@ const defaultFullCalendarConfig: CalendarOptions = {
dayMaxEventRows: true,
height: "parent",
eventDisplay: "list-item",
locales: allLocales,
views: {
list: {
visibleRange: getListWeekRange,
},
},
};
const viewButtons: ToggleButton[] = [
{ label: "Month View", value: "dayGridMonth", iconPath: mdiViewModule },
{ label: "Week View", value: "dayGridWeek", iconPath: mdiViewWeek },
{ label: "Day View", value: "dayGridDay", iconPath: mdiViewDay },
{ label: "List View", value: "listWeek", iconPath: mdiViewAgenda },
{ label: "List View", value: "list", iconPath: mdiViewAgenda },
];
class HAFullCalendar extends LitElement {
export class HAFullCalendar extends LitElement {
public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@ -79,6 +95,10 @@ class HAFullCalendar extends LitElement {
@internalProperty() private _activeView?: FullCalendarView;
public updateSize(): void {
this.calendar?.updateSize();
}
protected render(): TemplateResult {
const viewToggleButtons = this._viewToggleButtons(this.views);
@ -186,6 +206,12 @@ class HAFullCalendar extends LitElement {
this.calendar!.changeView(this._activeView);
this._fireViewChanged();
}
const oldHass = changedProps.get("hass") as HomeAssistant;
if (oldHass && oldHass.language !== this.hass.language) {
this.calendar.setOption("locale", this.hass.language);
}
}
protected firstUpdated(): void {
@ -243,7 +269,7 @@ class HAFullCalendar extends LitElement {
this._fireViewChanged();
}
private _handleView(ev): void {
private _handleView(ev: CustomEvent): void {
this._activeView = ev.detail.value;
this.calendar!.changeView(this._activeView!);
this._fireViewChanged();
@ -324,10 +350,19 @@ class HAFullCalendar extends LitElement {
#calendar {
flex-grow: 1;
background-color: var(--card-background-color);
background-color: var(
--ha-card-background,
var(--card-background-color, white)
);
min-height: 400px;
--fc-neutral-bg-color: var(--card-background-color);
--fc-list-event-hover-bg-color: var(--card-background-color);
--fc-neutral-bg-color: var(
--ha-card-background,
var(--card-background-color, white)
);
--fc-list-event-hover-bg-color: var(
--ha-card-background,
var(--card-background-color, white)
);
--fc-theme-standard-border-color: var(--divider-color);
--fc-border-color: var(--divider-color);
}
@ -487,6 +522,23 @@ class HAFullCalendar extends LitElement {
:host([narrow]) .fc-dayGridMonth-view .fc-scrollgrid-sync-table {
overflow: hidden;
}
.fc-scroller::-webkit-scrollbar {
width: 0.4rem;
height: 0.4rem;
}
.fc-scroller::-webkit-scrollbar-thumb {
-webkit-border-radius: 4px;
border-radius: 4px;
background: var(--scrollbar-thumb-color);
}
.fc-scroller {
overflow-y: auto;
scrollbar-color: var(--scrollbar-thumb-color) transparent;
scrollbar-width: thin;
}
`,
];
}

View File

@ -1,36 +1,33 @@
import {
customElement,
LitElement,
property,
internalProperty,
CSSResultArray,
css,
TemplateResult,
html,
PropertyValues,
} from "lit-element";
import { styleMap } from "lit-html/directives/style-map";
import "../../layouts/ha-app-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@material/mwc-checkbox";
import "@material/mwc-formfield";
import "../../components/ha-menu-button";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import {
css,
CSSResultArray,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { styleMap } from "lit-html/directives/style-map";
import { LocalStorage } from "../../common/decorators/local-storage";
import { HASSDomEvent } from "../../common/dom/fire_event";
import "../../components/ha-card";
import "./ha-full-calendar";
import "../../components/ha-menu-button";
import { fetchCalendarEvents, getCalendars } from "../../data/calendar";
import "../../layouts/ha-app-layout";
import { haStyle } from "../../resources/styles";
import type {
HomeAssistant,
SelectedCalendar,
Calendar,
CalendarEvent,
CalendarViewChanged,
Calendar,
HomeAssistant,
} from "../../types";
import { haStyle } from "../../resources/styles";
import { HASSDomEvent } from "../../common/dom/fire_event";
import { getCalendars, fetchCalendarEvents } from "../../data/calendar";
import "./ha-full-calendar";
@customElement("ha-panel-calendar")
class PanelCalendar extends LitElement {
@ -39,20 +36,20 @@ class PanelCalendar extends LitElement {
@property({ type: Boolean, reflect: true })
public narrow!: boolean;
@internalProperty() private _calendars: SelectedCalendar[] = [];
@internalProperty() private _calendars: Calendar[] = [];
@internalProperty() private _events: CalendarEvent[] = [];
@LocalStorage("deSelectedCalendars", true)
private _deSelectedCalendars: string[] = [];
private _start?: Date;
private _end?: Date;
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._calendars = getCalendars(this.hass).map((calendar) => ({
selected: true,
calendar,
}));
this._calendars = getCalendars(this.hass);
}
protected render(): TemplateResult {
@ -78,19 +75,22 @@ class PanelCalendar extends LitElement {
</div>
${this._calendars.map(
(selCal) =>
html`<div>
<mwc-formfield .label=${selCal.calendar.name}>
<mwc-checkbox
style=${styleMap({
"--mdc-theme-secondary": selCal.calendar
.backgroundColor!,
})}
.value=${selCal.calendar.entity_id}
.checked=${selCal.selected}
@change=${this._handleToggle}
></mwc-checkbox>
</mwc-formfield>
</div>`
html`
<div>
<mwc-formfield .label=${selCal.name}>
<mwc-checkbox
style=${styleMap({
"--mdc-theme-secondary": selCal.backgroundColor!,
})}
.value=${selCal.entity_id}
.checked=${!this._deSelectedCalendars.includes(
selCal.entity_id
)}
@change=${this._handleToggle}
></mwc-checkbox>
</mwc-formfield>
</div>
`
)}
</div>
<ha-full-calendar
@ -106,8 +106,8 @@ class PanelCalendar extends LitElement {
private get _selectedCalendars(): Calendar[] {
return this._calendars
.filter((selCal) => selCal.selected)
.map((cal) => cal.calendar);
.filter((selCal) => !this._deSelectedCalendars.includes(selCal.entity_id))
.map((cal) => cal);
}
private async _fetchEvents(
@ -124,24 +124,28 @@ class PanelCalendar extends LitElement {
private async _handleToggle(ev): Promise<void> {
const results = this._calendars.map(async (cal) => {
if (ev.target.value !== cal.calendar.entity_id) {
if (ev.target.value !== cal.entity_id) {
return cal;
}
const checked = ev.target.checked;
if (checked) {
const events = await this._fetchEvents(this._start!, this._end!, [
cal.calendar,
]);
const events = await this._fetchEvents(this._start!, this._end!, [cal]);
this._events = [...this._events, ...events];
this._deSelectedCalendars = this._deSelectedCalendars.filter(
(deCal) => deCal !== cal.entity_id
);
} else {
this._events = this._events.filter(
(event) => event.calendar !== cal.calendar.entity_id
(event) => event.calendar !== cal.entity_id
);
this._deSelectedCalendars = [
...this._deSelectedCalendars,
cal.entity_id,
];
}
cal.selected = checked;
return cal;
});
@ -179,7 +183,7 @@ class PanelCalendar extends LitElement {
}
:host(:not([narrow])) .content {
height: calc(100vh - var(--header-height);
height: calc(100vh - var(--header-height));
}
.calendar-list {

View File

@ -10,7 +10,8 @@ import {
internalProperty,
TemplateResult,
} from "lit-element";
import "../../../components/dialog/ha-paper-dialog";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-dialog";
import { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
@ -37,24 +38,27 @@ class DialogAreaDetail 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 entry = this._params.entry;
const nameInvalid = this._name.trim() === "";
const nameInvalid = !this._isNameValid();
return html`
<ha-paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${entry
? entry.name
: this.hass.localize("ui.panel.config.areas.editor.default_name")}
>
<h2>
${entry
? entry.name
: this.hass.localize("ui.panel.config.areas.editor.default_name")}
</h2>
<paper-dialog-scrollable>
<div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<div class="form">
${entry
@ -71,6 +75,7 @@ class DialogAreaDetail extends LitElement {
<paper-input
.value=${this._name}
@value-changed=${this._nameChanged}
@keyup=${this._handleKeyup}
.label=${this.hass.localize("ui.panel.config.areas.editor.name")}
.errorMessage=${this.hass.localize(
"ui.panel.config.areas.editor.name_required"
@ -78,32 +83,42 @@ class DialogAreaDetail extends LitElement {
.invalid=${nameInvalid}
></paper-input>
</div>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
${entry
? html`
<mwc-button
class="warning"
@click="${this._deleteEntry}"
.disabled=${this._submitting}
>
${this.hass.localize("ui.panel.config.areas.editor.delete")}
</mwc-button>
`
: html``}
<mwc-button
@click="${this._updateEntry}"
.disabled=${nameInvalid || this._submitting}
>
${entry
? this.hass.localize("ui.panel.config.areas.editor.update")
: this.hass.localize("ui.panel.config.areas.editor.create")}
</mwc-button>
</div>
</ha-paper-dialog>
${entry
? html`
<mwc-button
slot="secondaryAction"
class="warning"
@click="${this._deleteEntry}"
.disabled=${this._submitting}
>
${this.hass.localize("ui.panel.config.areas.editor.delete")}
</mwc-button>
`
: html``}
<mwc-button
slot="primaryAction"
@click="${this._updateEntry}"
.disabled=${nameInvalid || this._submitting}
>
${entry
? this.hass.localize("ui.panel.config.areas.editor.update")
: this.hass.localize("ui.panel.config.areas.editor.create")}
</mwc-button>
</ha-dialog>
`;
}
private _isNameValid() {
return this._name.trim() !== "";
}
private _handleKeyup(ev: KeyboardEvent) {
if (ev.keyCode === 13 && this._isNameValid() && !this._submitting) {
this._updateEntry();
}
}
private _nameChanged(ev: PolymerChangedEvent<string>) {
this._error = undefined;
this._name = ev.detail.value;
@ -141,12 +156,6 @@ class DialogAreaDetail extends LitElement {
}
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!(ev.detail as any).value) {
this._params = undefined;
}
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
@ -154,9 +163,6 @@ class DialogAreaDetail extends LitElement {
.form {
padding-bottom: 24px;
}
mwc-button.warning {
margin-right: auto;
}
.error {
color: var(--error-color);
}

View File

@ -81,7 +81,8 @@ class HaConfigAreaPage extends LitElement {
if (!area) {
return html`
<hass-error-screen
error="${this.hass.localize("ui.panel.config.areas.area_not_found")}"
.hass=${this.hass}
.error=${this.hass.localize("ui.panel.config.areas.area_not_found")}
></hass-error-screen>
`;
}
@ -312,8 +313,8 @@ class HaConfigAreaPage extends LitElement {
text: this.hass.localize(
"ui.panel.config.areas.delete.confirmation_text"
),
dismissText: this.hass.localize("ui.common.no"),
confirmText: this.hass.localize("ui.common.yes"),
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"),
}))
) {
return false;

View File

@ -126,9 +126,10 @@ export class HaConfigAreasDashboard extends LitElement {
></ha-icon-button>
<mwc-fab
slot="fab"
title="${this.hass.localize(
.label=${this.hass.localize(
"ui.panel.config.areas.picker.create_area"
)}"
)}
extended
@click=${this._createArea}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>

View File

@ -276,8 +276,8 @@ export default class HaAutomationActionRow extends LitElement {
text: this.hass.localize(
"ui.panel.config.automation.editor.actions.delete_confirm"
),
dismissText: this.hass.localize("ui.common.no"),
confirmText: this.hass.localize("ui.common.yes"),
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"),
confirm: () => {
fireEvent(this, "value-changed", { value: null });
},

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