Remove states-ui and allow setting (local) default lovelace panel (#5043)

* Remove states-ui and allow setting (local) default lovelace panel

* Remove from demo

* Delete ha-cards.js

* Add default for yaml defined dashboards

* Update ha-config-lovelace-dashboards.ts
This commit is contained in:
Bram Kragten 2020-03-03 16:27:35 +01:00 committed by GitHub
parent 1d1688093a
commit 7e48b21767
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 134 additions and 2492 deletions

View File

@ -3,6 +3,7 @@ name: Report a bug with the UI, Frontend or Lovelace
about: Report an issue related to the Home Assistant frontend.
labels: bug
---
<!-- READ THIS FIRST:
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
@ -10,6 +11,7 @@ labels: bug
- Provide as many details as possible. Paste logs, configuration samples and code into the backticks.
DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed without comment.
-->
## Checklist
- [ ] I have updated to the latest available Home Assistant version.
@ -17,21 +19,22 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
- [ ] I have tried a different browser to see if it is related to my browser.
## The problem
<!--
Describe the issue you are experiencing here to communicate to the
maintainers. Tell us about the current behavior.
If possible provide a screenshot with a description.
-->
## Expected behavior
<!--
<!--
Describe what you expected to happen or it should look/behave.
If possible provide a screenshot with a description.
-->
## Steps to reproduce
<!--
Provide steps for us, that helps reproducing your issue.
For example:
@ -43,8 +46,8 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
6. Set the HVAC action to cool
-->
## Environment
<!--
Provide details about the versions you are using, which helps us reproducing
and finding the issue quicker. Version information is found in the
@ -54,13 +57,13 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
your issue in a different browser and be sure to include your findings.
-->
- Home Assistant release with the issue:
- Last working Home Assistant release (if known):
- UI Type (States or Lovelace):
- Browser and browser version:
- Operating system:
- Home Assistant release with the issue:
- Last working Home Assistant release (if known):
- Browser and browser version:
- Operating system:
## Problem-relevant configuration
<!--
An example configuration that caused the problem for you. Fill this out even
if it seems unimportant to you. Please be sure to remove personal information
@ -72,6 +75,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
```
## Javascript errors shown in your browser console/inspector
<!--
If you come across any javascript or other error logs, e.g., in your browser
console/inspector please provide them.
@ -82,4 +86,3 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
```
## Additional information

View File

@ -11,17 +11,13 @@
"src/panels/dev-template/ha-panel-dev-template.js",
"src/panels/history/ha-panel-history.js",
"src/panels/iframe/ha-panel-iframe.js",
"src/panels/kiosk/ha-panel-kiosk.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"
],
"sources": [
"src/**/*",
"!src/translations/*"
],
"sources": ["src/**/*", "!src/translations/*"],
"lint": {
"rules": ["polymer-3"],
"ignoreWarnings": ["could-not-resolve-reference", "could-not-load"],

View File

@ -1,45 +0,0 @@
import { TemplateResult, html } from "lit-html";
import { customElement, LitElement, property } from "lit-element";
import { HassEntity } from "home-assistant-js-websocket";
import "../components/entity/ha-state-label-badge";
import { HomeAssistant } from "../types";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-badges-card")
class HaBadgesCard extends LitElement {
@property() public hass?: HomeAssistant;
@property() public states?: HassEntity[];
protected render(): TemplateResult {
if (!this.hass || !this.states) {
return html``;
}
return html`
${this.states.map(
(state) => html`
<ha-state-label-badge
.hass=${this.hass}
.state=${state}
@click=${this._handleClick}
></ha-state-label-badge>
`
)}
`;
}
private _handleClick(ev: Event) {
const entityId = ((ev.target as any).state as HassEntity).entity_id;
fireEvent(this, "hass-more-info", {
entityId,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-badges-card": HaBadgesCard;
}
}

View File

@ -1,127 +0,0 @@
import "@polymer/paper-styles/element-styles/paper-material-styles";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateName } from "../common/entity/compute_state_name";
import { EventsMixin } from "../mixins/events-mixin";
import LocalizeMixin from "../mixins/localize-mixin";
import { fetchThumbnailUrlWithCache } from "../data/camera";
const UPDATE_INTERVAL = 10000; // ms
/*
* @appliesMixin LocalizeMixin
* @appliesMixin EventsMixin
*/
class HaCameraCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="paper-material-styles">
:host {
@apply --paper-material-elevation-1;
display: block;
position: relative;
font-size: 0px;
border-radius: 2px;
cursor: pointer;
min-height: 48px;
line-height: 0;
}
.camera-feed {
width: 100%;
height: auto;
border-radius: 2px;
}
.caption {
@apply --paper-font-common-nowrap;
position: absolute;
left: 0px;
right: 0px;
bottom: 0px;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
background-color: rgba(0, 0, 0, 0.3);
padding: 16px;
font-size: 16px;
font-weight: 500;
line-height: 16px;
color: white;
}
</style>
<template is="dom-if" if="[[cameraFeedSrc]]">
<img
src="[[cameraFeedSrc]]"
class="camera-feed"
alt="[[_computeStateName(stateObj)]]"
on-load="_imageLoaded"
on-error="_imageError"
/>
</template>
<div class="caption">
[[_computeStateName(stateObj)]]
<template is="dom-if" if="[[!imageLoaded]]">
([[localize('ui.card.camera.not_available')]])
</template>
</div>
`;
}
static get properties() {
return {
hass: Object,
stateObj: {
type: Object,
observer: "updateCameraFeedSrc",
},
cameraFeedSrc: {
type: String,
value: "",
},
imageLoaded: {
type: Boolean,
value: true,
},
};
}
ready() {
super.ready();
this.addEventListener("click", () => this.cardTapped());
}
connectedCallback() {
super.connectedCallback();
this.timer = setInterval(() => this.updateCameraFeedSrc(), UPDATE_INTERVAL);
}
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.timer);
}
_imageLoaded() {
this.imageLoaded = true;
}
_imageError() {
this.imageLoaded = false;
}
cardTapped() {
this.fire("hass-more-info", { entityId: this.stateObj.entity_id });
}
async updateCameraFeedSrc() {
this.cameraFeedSrc = await fetchThumbnailUrlWithCache(
this.hass,
this.stateObj.entity_id
);
}
_computeStateName(stateObj) {
return computeStateName(stateObj);
}
}
customElements.define("ha-camera-card", HaCameraCard);

View File

@ -1,81 +0,0 @@
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./ha-camera-card";
import "./ha-entities-card";
import "./ha-history_graph-card";
import "./ha-media_player-card";
import "./ha-persistent_notification-card";
import "./ha-plant-card";
import "./ha-weather-card";
import dynamicContentUpdater from "../common/dom/dynamic_content_updater";
class HaCardChooser extends PolymerElement {
static get properties() {
return {
cardData: {
type: Object,
observer: "cardDataChanged",
},
};
}
_updateCard(newData) {
dynamicContentUpdater(
this,
"HA-" + newData.cardType.toUpperCase() + "-CARD",
newData
);
}
createObserver() {
this._updatesAllowed = false;
this.observer = new IntersectionObserver((entries) => {
if (!entries.length) return;
if (entries[0].isIntersecting) {
this.style.height = "";
if (this._detachedChild) {
this.appendChild(this._detachedChild);
this._detachedChild = null;
}
this._updateCard(this.cardData); // Don't use 'newData' as it might have changed.
this._updatesAllowed = true;
} else {
// Set the card to be 48px high. Otherwise if the card is kept as 0px height then all
// following cards would trigger the observer at once.
const offsetHeight = this.offsetHeight;
this.style.height = `${offsetHeight || 48}px`;
if (this.lastChild) {
this._detachedChild = this.lastChild;
this.removeChild(this.lastChild);
}
this._updatesAllowed = false;
}
});
this.observer.observe(this);
}
cardDataChanged(newData) {
if (!newData) return;
// ha-entities-card is exempt from observer as it doesn't load heavy resources.
// and usually doesn't load external resources (except for entity_picture).
const eligibleToObserver =
window.IntersectionObserver && newData.cardType !== "entities";
if (!eligibleToObserver) {
if (this.observer) {
this.observer.unobserve(this);
this.observer = null;
}
this.style.height = "";
this._updateCard(newData);
return;
}
if (!this.observer) {
this.createObserver();
}
if (this._updatesAllowed) {
this._updateCard(newData);
}
}
}
customElements.define("ha-card-chooser", HaCardChooser);

View File

@ -1,182 +0,0 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/entity/ha-entity-toggle";
import "../components/ha-card";
import "../state-summary/state-card-content";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { stateMoreInfoType } from "../common/entity/state_more_info_type";
import { canToggleState } from "../common/entity/can_toggle_state";
import { EventsMixin } from "../mixins/events-mixin";
import LocalizeMixin from "../mixins/localize-mixin";
class HaEntitiesCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex"></style>
<style>
ha-card {
padding: 16px;
}
.states {
margin: -4px 0;
}
.state {
padding: 4px 0;
}
.header {
@apply --paper-font-headline;
/* overwriting line-height +8 because entity-toggle can be 40px height,
compensating this with reduced padding */
line-height: 40px;
color: var(--primary-text-color);
padding: 4px 0 12px;
}
.header .name {
@apply --paper-font-common-nowrap;
}
ha-entity-toggle {
margin-left: 16px;
}
.more-info {
cursor: pointer;
}
</style>
<ha-card>
<template is="dom-if" if="[[title]]">
<div
class$="[[computeTitleClass(groupEntity)]]"
on-click="entityTapped"
>
<div class="flex name">[[title]]</div>
<template is="dom-if" if="[[showGroupToggle(groupEntity, states)]]">
<ha-entity-toggle
hass="[[hass]]"
state-obj="[[groupEntity]]"
></ha-entity-toggle>
</template>
</div>
</template>
<div class="states">
<template
is="dom-repeat"
items="[[states]]"
on-dom-change="addTapEvents"
>
<div class$="[[computeStateClass(item)]]">
<state-card-content
hass="[[hass]]"
class="state-card"
state-obj="[[item]]"
></state-card-content>
</div>
</template>
</div>
</ha-card>
`;
}
static get properties() {
return {
hass: Object,
states: Array,
groupEntity: Object,
title: {
type: String,
computed: "computeTitle(states, groupEntity, localize)",
},
};
}
constructor() {
super();
// We need to save a single bound function reference to ensure the event listener
// can identify it properly.
this.entityTapped = this.entityTapped.bind(this);
}
computeTitle(states, groupEntity, localize) {
if (groupEntity) {
return computeStateName(groupEntity).trim();
}
const domain = computeStateDomain(states[0]);
return (
(localize && localize(`domain.${domain}`)) || domain.replace(/_/g, " ")
);
}
computeTitleClass(groupEntity) {
let classes = "header horizontal layout center ";
if (groupEntity) {
classes += "more-info";
}
return classes;
}
computeStateClass(stateObj) {
return stateMoreInfoType(stateObj) !== "hidden"
? "state more-info"
: "state";
}
addTapEvents() {
const cards = this.root.querySelectorAll(".state");
cards.forEach((card) => {
if (card.classList.contains("more-info")) {
card.addEventListener("click", this.entityTapped);
} else {
card.removeEventListener("click", this.entityTapped);
}
});
}
entityTapped(ev) {
const item = this.root
.querySelector("dom-repeat")
.itemForElement(ev.target);
let entityId;
if (!item && !this.groupEntity) {
return;
}
ev.stopPropagation();
if (item) {
entityId = item.entity_id;
} else {
entityId = this.groupEntity.entity_id;
}
this.fire("hass-more-info", { entityId: entityId });
}
showGroupToggle(groupEntity, states) {
if (
!groupEntity ||
!states ||
groupEntity.attributes.control === "hidden" ||
(groupEntity.state !== "on" && groupEntity.state !== "off")
) {
return false;
}
// Only show if we can toggle 2+ entities in group
let canToggleCount = 0;
for (let i = 0; i < states.length; i++) {
if (!canToggleState(this.hass, states[i])) {
continue;
}
canToggleCount++;
if (canToggleCount > 1) {
break;
}
}
return canToggleCount > 1;
}
}
customElements.define("ha-entities-card", HaEntitiesCard);

View File

@ -1,409 +0,0 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-progress/paper-progress";
import "@polymer/paper-styles/element-styles/paper-material-styles";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import HassMediaPlayerEntity from "../util/hass-media-player-model";
import { fetchMediaPlayerThumbnailWithCache } from "../data/media-player";
import { computeStateName } from "../common/entity/compute_state_name";
import { EventsMixin } from "../mixins/events-mixin";
import LocalizeMixin from "../mixins/localize-mixin";
/*
* @appliesMixin LocalizeMixin
* @appliesMixin EventsMixin
*/
class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style
include="paper-material-styles iron-flex iron-flex-alignment iron-positioning"
>
:host {
@apply --paper-material-elevation-1;
display: block;
position: relative;
font-size: 0px;
border-radius: 2px;
}
.banner {
position: relative;
background-color: white;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.banner:before {
display: block;
content: "";
width: 100%;
/* removed .25% from 16:9 ratio to fix YT black bars */
padding-top: 56%;
transition: padding-top 0.8s;
}
.banner.no-cover {
background-position: center center;
background-image: url(/static/images/card_media_player_bg.png);
background-repeat: no-repeat;
background-color: var(--primary-color);
}
.banner.content-type-music:before {
padding-top: 100%;
}
.banner.content-type-game:before {
padding-top: 100%;
}
.banner.no-cover:before {
padding-top: 88px;
}
.banner > .cover {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
background-position: center center;
background-size: cover;
transition: opacity 0.8s;
opacity: 1;
}
.banner.is-off > .cover {
opacity: 0;
}
.banner > .caption {
@apply --paper-font-caption;
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, var(--dark-secondary-opacity));
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
color: white;
transition: background-color 0.5s;
}
.banner.is-off > .caption {
background-color: initial;
}
.banner > .caption .title {
@apply --paper-font-common-nowrap;
font-size: 1.2em;
margin: 8px 0 4px;
}
.progress {
width: 100%;
height: var(--paper-progress-height, 4px);
margin-top: calc(-1 * var(--paper-progress-height, 4px));
--paper-progress-active-color: var(--accent-color);
--paper-progress-container-color: rgba(200, 200, 200, 0.5);
}
.controls {
position: relative;
@apply --paper-font-body1;
padding: 8px;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
background-color: var(--paper-card-background-color, white);
}
.controls paper-icon-button {
width: 44px;
height: 44px;
}
.playback-controls {
direction: ltr;
}
paper-icon-button {
opacity: var(--dark-primary-opacity);
}
paper-icon-button[disabled] {
opacity: var(--dark-disabled-opacity);
}
paper-icon-button.primary {
width: 56px !important;
height: 56px !important;
background-color: var(--primary-color);
color: white;
border-radius: 50%;
padding: 8px;
transition: background-color 0.5s;
}
paper-icon-button.primary[disabled] {
background-color: rgba(0, 0, 0, var(--dark-disabled-opacity));
}
[invisible] {
visibility: hidden !important;
}
</style>
<div
class$="[[computeBannerClasses(playerObj, _coverShowing, _coverLoadError)]]"
>
<div class="cover" id="cover"></div>
<div class="caption">
[[_computeStateName(stateObj)]]
<div class="title">[[computePrimaryText(localize, playerObj)]]</div>
[[playerObj.secondaryTitle]]<br />
</div>
</div>
<paper-progress
max="[[stateObj.attributes.media_duration]]"
value="[[playbackPosition]]"
hidden$="[[computeHideProgress(playerObj)]]"
class="progress"
></paper-progress>
<div class="controls layout horizontal justified">
<paper-icon-button
aria-label="Turn off"
icon="hass:power"
on-click="handleTogglePower"
invisible$="[[computeHidePowerButton(playerObj)]]"
class="self-center secondary"
></paper-icon-button>
<div class="playback-controls">
<paper-icon-button
aria-label="Previous track"
icon="hass:skip-previous"
invisible$="[[!playerObj.supportsPreviousTrack]]"
disabled="[[playerObj.isOff]]"
on-click="handlePrevious"
></paper-icon-button>
<paper-icon-button
aria-label="Play or Pause"
class="primary"
icon="[[computePlaybackControlIcon(playerObj)]]"
invisible$="[[!computePlaybackControlIcon(playerObj)]]"
disabled="[[playerObj.isOff]]"
on-click="handlePlaybackControl"
></paper-icon-button>
<paper-icon-button
aria-label="Next track"
icon="hass:skip-next"
invisible$="[[!playerObj.supportsNextTrack]]"
disabled="[[playerObj.isOff]]"
on-click="handleNext"
></paper-icon-button>
</div>
<paper-icon-button
aria-label="More information."
icon="hass:dots-vertical"
on-click="handleOpenMoreInfo"
class="self-center secondary"
></paper-icon-button>
</div>
`;
}
static get properties() {
return {
hass: Object,
stateObj: Object,
playerObj: {
type: Object,
computed: "computePlayerObj(hass, stateObj)",
observer: "playerObjChanged",
},
playbackControlIcon: {
type: String,
computed: "computePlaybackControlIcon(playerObj)",
},
playbackPosition: Number,
_coverShowing: {
type: Boolean,
value: false,
},
_coverLoadError: {
type: Boolean,
value: false,
},
};
}
async playerObjChanged(playerObj, oldPlayerObj) {
if (playerObj.isPlaying && playerObj.showProgress) {
if (!this._positionTracking) {
this._positionTracking = setInterval(
() => this.updatePlaybackPosition(),
1000
);
}
} else if (this._positionTracking) {
clearInterval(this._positionTracking);
this._positionTracking = null;
}
if (playerObj.showProgress) {
this.updatePlaybackPosition();
}
const picture = playerObj.stateObj.attributes.entity_picture;
const oldPicture =
oldPlayerObj && oldPlayerObj.stateObj.attributes.entity_picture;
if (picture !== oldPicture && !picture) {
this.$.cover.style.backgroundImage = "";
return;
}
if (picture === oldPicture) {
return;
}
// We have a new picture url
// If entity picture is non-relative, we use that url directly.
if (picture.substr(0, 1) !== "/") {
this._coverShowing = true;
this._coverLoadError = false;
this.$.cover.style.backgroundImage = `url(${picture})`;
return;
}
try {
const {
content_type: contentType,
content,
} = await fetchMediaPlayerThumbnailWithCache(
this.hass,
playerObj.stateObj.entity_id
);
this._coverShowing = true;
this._coverLoadError = false;
this.$.cover.style.backgroundImage = `url(data:${contentType};base64,${content})`;
} catch (err) {
this._coverShowing = false;
this._coverLoadError = true;
this.$.cover.style.backgroundImage = "";
}
}
updatePlaybackPosition() {
this.playbackPosition = this.playerObj.currentProgress;
}
computeBannerClasses(playerObj, coverShowing, coverLoadError) {
var cls = "banner";
if (!playerObj) {
return cls;
}
if (playerObj.isOff || playerObj.isIdle) {
cls += " is-off no-cover";
} else if (
!playerObj.stateObj.attributes.entity_picture ||
coverLoadError ||
!coverShowing
) {
cls += " no-cover";
} else if (playerObj.stateObj.attributes.media_content_type === "music") {
cls += " content-type-music";
} else if (playerObj.stateObj.attributes.media_content_type === "game") {
cls += " content-type-game";
}
return cls;
}
computeHideProgress(playerObj) {
return !playerObj.showProgress;
}
computeHidePowerButton(playerObj) {
return playerObj.isOff
? !playerObj.supportsTurnOn
: !playerObj.supportsTurnOff;
}
computePlayerObj(hass, stateObj) {
return new HassMediaPlayerEntity(hass, stateObj);
}
computePrimaryText(localize, playerObj) {
return (
playerObj.primaryTitle ||
localize(`state.media_player.${playerObj.stateObj.state}`) ||
localize(`state.default.${playerObj.stateObj.state}`) ||
playerObj.stateObj.state
);
}
computePlaybackControlIcon(playerObj) {
if (playerObj.isPlaying) {
return playerObj.supportsPause ? "hass:pause" : "hass:stop";
}
if (playerObj.hasMediaControl || playerObj.isOff || playerObj.isIdle) {
if (
playerObj.hasMediaControl &&
playerObj.supportsPause &&
!playerObj.isPaused
) {
return "hass:play-pause";
}
return playerObj.supportsPlay ? "hass:play" : null;
}
return "";
}
_computeStateName(stateObj) {
return computeStateName(stateObj);
}
handleNext(ev) {
ev.stopPropagation();
this.playerObj.nextTrack();
}
handleOpenMoreInfo(ev) {
ev.stopPropagation();
this.fire("hass-more-info", { entityId: this.stateObj.entity_id });
}
handlePlaybackControl(ev) {
ev.stopPropagation();
this.playerObj.mediaPlayPause();
}
handlePrevious(ev) {
ev.stopPropagation();
this.playerObj.previousTrack();
}
handleTogglePower(ev) {
ev.stopPropagation();
this.playerObj.togglePower();
}
}
customElements.define("ha-media_player-card", HaMediaPlayerCard);

View File

@ -1,76 +0,0 @@
import "@material/mwc-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/ha-card";
import "../components/ha-markdown";
import { computeStateName } from "../common/entity/compute_state_name";
import LocalizeMixin from "../mixins/localize-mixin";
import { computeObjectId } from "../common/entity/compute_object_id";
/*
* @appliesMixin LocalizeMixin
*/
class HaPersistentNotificationCard extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style>
:host {
@apply --paper-font-body1;
}
ha-markdown {
display: block;
padding: 0 16px;
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
}
ha-markdown p:first-child {
margin-top: 0;
}
ha-markdown p:last-child {
margin-bottom: 0;
}
ha-markdown a {
color: var(--primary-color);
}
ha-markdown img {
max-width: 100%;
}
mwc-button {
margin: 8px;
}
</style>
<ha-card header="[[computeTitle(stateObj)]]">
<ha-markdown content="[[stateObj.attributes.message]]"></ha-markdown>
<mwc-button on-click="dismissTap"
>[[localize('ui.card.persistent_notification.dismiss')]]</mwc-button
>
</ha-card>
`;
}
static get properties() {
return {
hass: Object,
stateObj: Object,
};
}
computeTitle(stateObj) {
return stateObj.attributes.title || computeStateName(stateObj);
}
dismissTap(ev) {
ev.preventDefault();
this.hass.callService("persistent_notification", "dismiss", {
notification_id: computeObjectId(this.stateObj.entity_id),
});
}
}
customElements.define(
"ha-persistent_notification-card",
HaPersistentNotificationCard
);

View File

@ -1,165 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/ha-card";
import "../components/ha-icon";
import { computeStateName } from "../common/entity/compute_state_name";
import { EventsMixin } from "../mixins/events-mixin";
class HaPlantCard extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style>
.banner {
display: flex;
align-items: flex-end;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
padding-top: 12px;
}
.has-plant-image .banner {
padding-top: 30%;
}
.header {
@apply --paper-font-headline;
line-height: 40px;
padding: 8px 16px;
}
.has-plant-image .header {
font-size: 16px;
font-weight: 500;
line-height: 16px;
padding: 16px;
color: white;
width: 100%;
background: rgba(0, 0, 0, var(--dark-secondary-opacity));
}
.content {
display: flex;
justify-content: space-between;
padding: 16px 32px 24px 32px;
}
.has-plant-image .content {
padding-bottom: 16px;
}
ha-icon {
color: var(--paper-item-icon-color);
margin-bottom: 8px;
}
.attributes {
cursor: pointer;
}
.attributes div {
text-align: center;
}
.problem {
color: var(--google-red-500);
font-weight: bold;
}
.uom {
color: var(--secondary-text-color);
}
</style>
<ha-card
class$="[[computeImageClass(stateObj.attributes.entity_picture)]]"
>
<div
class="banner"
style="background-image:url([[stateObj.attributes.entity_picture]])"
>
<div class="header">[[computeTitle(stateObj)]]</div>
</div>
<div class="content">
<template
is="dom-repeat"
items="[[computeAttributes(stateObj.attributes)]]"
>
<div class="attributes" on-click="attributeClicked">
<div>
<ha-icon
icon="[[computeIcon(item, stateObj.attributes.battery)]]"
></ha-icon>
</div>
<div
class$="[[computeAttributeClass(stateObj.attributes.problem, item)]]"
>
[[computeValue(stateObj.attributes, item)]]
</div>
<div class="uom">
[[computeUom(stateObj.attributes.unit_of_measurement_dict,
item)]]
</div>
</div>
</template>
</div>
</ha-card>
`;
}
static get properties() {
return {
hass: Object,
stateObj: Object,
config: Object,
};
}
constructor() {
super();
this.sensors = {
moisture: "hass:water",
temperature: "hass:thermometer",
brightness: "hass:white-balance-sunny",
conductivity: "hass:emoticon-poop",
battery: "hass:battery",
};
}
computeTitle(stateObj) {
return (this.config && this.config.name) || computeStateName(stateObj);
}
computeAttributes(data) {
return Object.keys(this.sensors).filter((key) => key in data);
}
computeIcon(attr, batLvl) {
const icon = this.sensors[attr];
if (attr === "battery") {
if (batLvl <= 5) {
return `${icon}-alert`;
}
if (batLvl < 95) {
return `${icon}-${Math.round(batLvl / 10 - 0.01) * 10}`;
}
}
return icon;
}
computeValue(attributes, attr) {
return attributes[attr];
}
computeUom(dict, attr) {
return dict[attr] || "";
}
computeAttributeClass(problem, attr) {
return problem.indexOf(attr) === -1 ? "" : "problem";
}
computeImageClass(entityPicture) {
return entityPicture ? "has-plant-image" : "";
}
attributeClicked(ev) {
this.fire("hass-more-info", {
entityId: this.stateObj.attributes.sensors[ev.model.item],
});
}
}
customElements.define("ha-plant-card", HaPlantCard);

View File

@ -1,383 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateName } from "../common/entity/compute_state_name";
import "../components/ha-card";
import "../components/ha-icon";
import { EventsMixin } from "../mixins/events-mixin";
import LocalizeMixin from "../mixins/localize-mixin";
import { computeRTL } from "../common/util/compute_rtl";
/*
* @appliesMixin LocalizeMixin
*/
class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style>
:host {
cursor: pointer;
}
.content {
padding: 0 20px 20px;
}
ha-icon {
color: var(--paper-item-icon-color);
}
.header {
font-family: var(--paper-font-headline_-_font-family);
-webkit-font-smoothing: var(
--paper-font-headline_-_-webkit-font-smoothing
);
font-size: var(--paper-font-headline_-_font-size);
font-weight: var(--paper-font-headline_-_font-weight);
letter-spacing: var(--paper-font-headline_-_letter-spacing);
line-height: var(--paper-font-headline_-_line-height);
text-rendering: var(
--paper-font-common-expensive-kerning_-_text-rendering
);
opacity: var(--dark-primary-opacity);
padding: 24px 16px 16px;
display: flex;
align-items: baseline;
}
.name {
margin-left: 16px;
font-size: 16px;
color: var(--secondary-text-color);
}
:host([rtl]) .name {
margin-left: 0px;
margin-right: 16px;
}
.now {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
.main {
display: flex;
align-items: center;
margin-right: 32px;
}
:host([rtl]) .main {
margin-right: 0px;
}
.main ha-icon {
--iron-icon-height: 72px;
--iron-icon-width: 72px;
margin-right: 8px;
}
:host([rtl]) .main ha-icon {
margin-right: 0px;
}
.main .temp {
font-size: 52px;
line-height: 1em;
position: relative;
}
:host([rtl]) .main .temp {
direction: ltr;
margin-right: 28px;
}
.main .temp span {
font-size: 24px;
line-height: 1em;
position: absolute;
top: 4px;
}
.measurand {
display: inline-block;
}
:host([rtl]) .measurand {
direction: ltr;
}
.forecast {
margin-top: 16px;
display: flex;
justify-content: space-between;
}
.forecast div {
flex: 0 0 auto;
text-align: center;
}
.forecast .icon {
margin: 4px 0;
text-align: center;
}
:host([rtl]) .forecast .temp {
direction: ltr;
}
.weekday {
font-weight: bold;
}
.attributes,
.templow,
.precipitation {
color: var(--secondary-text-color);
}
:host([rtl]) .precipitation {
direction: ltr;
}
</style>
<ha-card>
<div class="header">
[[computeState(stateObj.state, localize)]]
<div class="name">[[computeName(stateObj)]]</div>
</div>
<div class="content">
<div class="now">
<div class="main">
<template is="dom-if" if="[[showWeatherIcon(stateObj.state)]]">
<ha-icon icon="[[getWeatherIcon(stateObj.state)]]"></ha-icon>
</template>
<div class="temp">
[[stateObj.attributes.temperature]]<span
>[[getUnit('temperature')]]</span
>
</div>
</div>
<div class="attributes">
<template
is="dom-if"
if="[[_showValue(stateObj.attributes.pressure)]]"
>
<div>
[[localize('ui.card.weather.attributes.air_pressure')]]:
<span class="measurand">
[[stateObj.attributes.pressure]] [[getUnit('air_pressure')]]
</span>
</div>
</template>
<template
is="dom-if"
if="[[_showValue(stateObj.attributes.humidity)]]"
>
<div>
[[localize('ui.card.weather.attributes.humidity')]]:
<span class="measurand"
>[[stateObj.attributes.humidity]] %</span
>
</div>
</template>
<template
is="dom-if"
if="[[_showValue(stateObj.attributes.wind_speed)]]"
>
<div>
[[localize('ui.card.weather.attributes.wind_speed')]]:
<span class="measurand">
[[getWindSpeed(stateObj.attributes.wind_speed)]]
</span>
[[getWindBearing(stateObj.attributes.wind_bearing, localize)]]
</div>
</template>
</div>
</div>
<template is="dom-if" if="[[forecast]]">
<div class="forecast">
<template is="dom-repeat" items="[[forecast]]">
<div>
<div class="weekday">
[[computeDate(item.datetime)]]<br />
<template is="dom-if" if="[[!_showValue(item.templow)]]">
[[computeTime(item.datetime)]]
</template>
</div>
<template is="dom-if" if="[[_showValue(item.condition)]]">
<div class="icon">
<ha-icon
icon="[[getWeatherIcon(item.condition)]]"
></ha-icon>
</div>
</template>
<template is="dom-if" if="[[_showValue(item.temperature)]]">
<div class="temp">
[[item.temperature]] [[getUnit('temperature')]]
</div>
</template>
<template is="dom-if" if="[[_showValue(item.templow)]]">
<div class="templow">
[[item.templow]] [[getUnit('temperature')]]
</div>
</template>
<template is="dom-if" if="[[_showValue(item.precipitation)]]">
<div class="precipitation">
[[item.precipitation]] [[getUnit('precipitation')]]
</div>
</template>
</div>
</template>
</div>
</template>
</div>
</ha-card>
`;
}
static get properties() {
return {
hass: Object,
config: Object,
stateObj: Object,
forecast: {
type: Array,
computed: "computeForecast(stateObj.attributes.forecast)",
},
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "_computeRTL(hass)",
},
};
}
constructor() {
super();
this.cardinalDirections = [
"N",
"NNE",
"NE",
"ENE",
"E",
"ESE",
"SE",
"SSE",
"S",
"SSW",
"SW",
"WSW",
"W",
"WNW",
"NW",
"NNW",
"N",
];
this.weatherIcons = {
"clear-night": "hass:weather-night",
cloudy: "hass:weather-cloudy",
exceptional: "hass:alert-circle-outline",
fog: "hass:weather-fog",
hail: "hass:weather-hail",
lightning: "hass:weather-lightning",
"lightning-rainy": "hass:weather-lightning-rainy",
partlycloudy: "hass:weather-partly-cloudy",
pouring: "hass:weather-pouring",
rainy: "hass:weather-rainy",
snowy: "hass:weather-snowy",
"snowy-rainy": "hass:weather-snowy-rainy",
sunny: "hass:weather-sunny",
windy: "hass:weather-windy",
"windy-variant": "hass:weather-windy-variant",
};
}
ready() {
this.addEventListener("click", this._onClick);
super.ready();
}
_onClick() {
this.fire("hass-more-info", { entityId: this.stateObj.entity_id });
}
computeForecast(forecast) {
return forecast && forecast.slice(0, 5);
}
getUnit(measure) {
const lengthUnit = this.hass.config.unit_system.length || "";
switch (measure) {
case "air_pressure":
return lengthUnit === "km" ? "hPa" : "inHg";
case "length":
return lengthUnit;
case "precipitation":
return lengthUnit === "km" ? "mm" : "in";
default:
return this.hass.config.unit_system[measure] || "";
}
}
computeState(state, localize) {
return localize(`state.weather.${state}`) || state;
}
computeName(stateObj) {
return (this.config && this.config.name) || computeStateName(stateObj);
}
showWeatherIcon(condition) {
return condition in this.weatherIcons;
}
getWeatherIcon(condition) {
return this.weatherIcons[condition];
}
windBearingToText(degree) {
const degreenum = parseInt(degree);
if (isFinite(degreenum)) {
return this.cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16];
}
return degree;
}
getWindSpeed(speed) {
return `${speed} ${this.getUnit("length")}/h`;
}
getWindBearing(bearing, localize) {
if (bearing != null) {
const cardinalDirection = this.windBearingToText(bearing);
return `(${localize(
`ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
) || cardinalDirection})`;
}
return ``;
}
_showValue(item) {
return typeof item !== "undefined" && item !== null;
}
computeDate(data) {
const date = new Date(data);
return date.toLocaleDateString(this.hass.language, { weekday: "short" });
}
computeTime(data) {
const date = new Date(data);
return date.toLocaleTimeString(this.hass.language, { hour: "numeric" });
}
_computeRTL(hass) {
return computeRTL(hass);
}
}
customElements.define("ha-weather-card", HaWeatherCard);

View File

@ -1,374 +0,0 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import { timeOut } from "@polymer/polymer/lib/utils/async";
import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../cards/ha-badges-card";
import "../cards/ha-card-chooser";
import "./ha-demo-badge";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { splitByGroups } from "../common/entity/split_by_groups";
import { getGroupEntities } from "../common/entity/get_group_entities";
// mapping domain to size of the card.
const DOMAINS_WITH_CARD = {
camera: 4,
history_graph: 4,
media_player: 3,
persistent_notification: 0,
plant: 3,
weather: 4,
};
// 4 types:
// badges: 0 .. 10
// before groups < 0
// groups: X
// rest: 100
const PRIORITY = {
// before groups < 0
configurator: -20,
persistent_notification: -15,
// badges have priority >= 0
updater: 0,
sun: 1,
person: 2,
device_tracker: 3,
alarm_control_panel: 4,
timer: 5,
sensor: 6,
binary_sensor: 7,
mailbox: 8,
};
const getPriority = (domain) => (domain in PRIORITY ? PRIORITY[domain] : 100);
const sortPriority = (domainA, domainB) => domainA.priority - domainB.priority;
const entitySortBy = (entityA, entityB) => {
const nameA = (
entityA.attributes.friendly_name || entityA.entity_id
).toLowerCase();
const nameB = (
entityB.attributes.friendly_name || entityB.entity_id
).toLowerCase();
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
};
const iterateDomainSorted = (collection, func) => {
Object.keys(collection)
.map((key) => collection[key])
.sort(sortPriority)
.forEach((domain) => {
domain.states.sort(entitySortBy);
func(domain);
});
};
class HaCards extends PolymerElement {
static get template() {
return html`
<style include="iron-flex iron-flex-factors"></style>
<style>
:host {
display: block;
padding: 4px 4px 0;
transform: translateZ(0);
position: relative;
}
.badges {
font-size: 85%;
text-align: center;
padding-top: 16px;
}
.column {
max-width: 500px;
overflow-x: hidden;
}
ha-card-chooser {
display: block;
margin: 4px 4px 8px;
}
@media (max-width: 500px) {
:host {
padding-left: 0;
padding-right: 0;
}
ha-card-chooser {
margin-left: 0;
margin-right: 0;
}
}
@media (max-width: 599px) {
.column {
max-width: 600px;
}
}
</style>
<div id="main">
<template is="dom-if" if="[[cards.badges.length]]">
<div class="badges">
<template is="dom-if" if="[[cards.demo]]">
<ha-demo-badge></ha-demo-badge>
</template>
<ha-badges-card
states="[[cards.badges]]"
hass="[[hass]]"
></ha-badges-card>
</div>
</template>
<div class="horizontal layout center-justified">
<template is="dom-repeat" items="[[cards.columns]]" as="column">
<div class="column flex-1">
<template is="dom-repeat" items="[[column]]" as="card">
<ha-card-chooser card-data="[[card]]"></ha-card-chooser>
</template>
</div>
</template>
</div>
</div>
`;
}
static get properties() {
return {
hass: Object,
columns: {
type: Number,
value: 2,
},
states: Object,
viewVisible: {
type: Boolean,
value: false,
},
orderedGroupEntities: Array,
cards: Object,
};
}
static get observers() {
return ["updateCards(columns, states, viewVisible, orderedGroupEntities)"];
}
updateCards(columns, states, viewVisible, orderedGroupEntities) {
if (!viewVisible) {
if (this.$.main.parentNode) {
this.$.main._parentNode = this.$.main.parentNode;
this.$.main.parentNode.removeChild(this.$.main);
}
return;
}
if (!this.$.main.parentNode && this.$.main._parentNode) {
this.$.main._parentNode.appendChild(this.$.main);
}
this._debouncer = Debouncer.debounce(
this._debouncer,
timeOut.after(10),
() => {
// Things might have changed since it got scheduled.
if (this.viewVisible) {
this.cards = this.computeCards(columns, states, orderedGroupEntities);
}
}
);
}
emptyCards() {
return {
demo: false,
badges: [],
columns: [],
};
}
computeCards(columns, states, orderedGroupEntities) {
const hass = this.hass;
const cards = this.emptyCards();
const entityCount = [];
for (let i = 0; i < columns; i++) {
cards.columns.push([]);
entityCount.push(0);
}
// Find column with < 5 entities, else column with lowest count
function getIndex(size) {
let minIndex = 0;
for (let i = 0; i < entityCount.length; i++) {
if (entityCount[i] < 5) {
minIndex = i;
break;
}
if (entityCount[i] < entityCount[minIndex]) {
minIndex = i;
}
}
entityCount[minIndex] += size;
return minIndex;
}
function addEntitiesCard(name, entities, groupEntity) {
if (entities.length === 0) return;
const owncard = [];
const other = [];
let size = 0;
entities.forEach((entity) => {
const domain = computeStateDomain(entity);
if (
domain in DOMAINS_WITH_CARD &&
!entity.attributes.custom_ui_state_card
) {
owncard.push(entity);
size += DOMAINS_WITH_CARD[domain];
} else {
other.push(entity);
size++;
}
});
// Add 1 to the size if we're rendering entities card
size += other.length > 0;
const curIndex = getIndex(size);
if (other.length > 0) {
cards.columns[curIndex].push({
hass: hass,
cardType: "entities",
states: other,
groupEntity: groupEntity || false,
});
}
owncard.forEach((entity) => {
cards.columns[curIndex].push({
hass: hass,
cardType: computeStateDomain(entity),
stateObj: entity,
});
});
}
const splitted = splitByGroups(states);
if (orderedGroupEntities) {
splitted.groups.sort(
(gr1, gr2) =>
orderedGroupEntities[gr1.entity_id] -
orderedGroupEntities[gr2.entity_id]
);
} else {
splitted.groups.sort(
(gr1, gr2) => gr1.attributes.order - gr2.attributes.order
);
}
const badgesColl = {};
const beforeGroupColl = {};
const afterGroupedColl = {};
Object.keys(splitted.ungrouped).forEach((key) => {
const state = splitted.ungrouped[key];
const domain = computeStateDomain(state);
if (domain === "a") {
cards.demo = true;
return;
}
const priority = getPriority(domain);
let coll;
if (priority < 0) {
coll = beforeGroupColl;
} else if (priority < 10) {
coll = badgesColl;
} else {
coll = afterGroupedColl;
}
if (!(domain in coll)) {
coll[domain] = {
domain: domain,
priority: priority,
states: [],
};
}
coll[domain].states.push(state);
});
if (orderedGroupEntities) {
Object.keys(badgesColl)
.map((key) => badgesColl[key])
.forEach((domain) => {
cards.badges.push.apply(cards.badges, domain.states);
});
cards.badges.sort(
(e1, e2) =>
orderedGroupEntities[e1.entity_id] -
orderedGroupEntities[e2.entity_id]
);
} else {
iterateDomainSorted(badgesColl, (domain) => {
cards.badges.push.apply(cards.badges, domain.states);
});
}
iterateDomainSorted(beforeGroupColl, (domain) => {
addEntitiesCard(domain.domain, domain.states);
});
splitted.groups.forEach((groupState) => {
const entities = getGroupEntities(states, groupState);
addEntitiesCard(
groupState.entity_id,
Object.keys(entities).map((key) => entities[key]),
groupState
);
});
iterateDomainSorted(afterGroupedColl, (domain) => {
addEntitiesCard(domain.domain, domain.states);
});
// Remove empty columns
cards.columns = cards.columns.filter((val) => val.length > 0);
return cards;
}
}
customElements.define("ha-cards", HaCards);

View File

@ -79,6 +79,7 @@ const panelSorter = (a: PanelInfo, b: PanelInfo) => {
}
return 0;
};
const DEFAULT_PAGE = localStorage.defaultPage || DEFAULT_PANEL;
const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
const panels = hass.panels;
@ -90,7 +91,7 @@ const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => {
const afterSpacer: PanelInfo[] = [];
Object.values(panels).forEach((panel) => {
if (!panel.title) {
if (!panel.title || panel.url_path === DEFAULT_PAGE) {
return;
}
(SHOW_AFTER_SPACER.includes(panel.url_path)
@ -114,8 +115,7 @@ class HaSidebar extends LitElement {
@property({ type: Boolean }) public alwaysExpand = false;
@property({ type: Boolean, reflect: true }) public expanded = false;
@property() public _defaultPage?: string =
localStorage.defaultPage || DEFAULT_PANEL;
@property() private _externalConfig?: ExternalConfig;
@property() private _notifications?: PersistentNotification[];
// property used only in css
@ -144,6 +144,9 @@ class HaSidebar extends LitElement {
}
}
const defaultPanel =
this.hass.panels[DEFAULT_PAGE] || this.hass.panels[DEFAULT_PANEL];
return html`
<div class="menu">
${!this.narrow
@ -168,9 +171,9 @@ class HaSidebar extends LitElement {
@keydown=${this._listboxKeydown}
>
${this._renderPanel(
this._defaultPage,
"hass:apps",
hass.localize("panel.states")
defaultPanel.url_path,
defaultPanel.icon || "hass:apps",
defaultPanel.title || hass.localize("panel.states")
)}
${beforeSpacer.map((panel) =>
this._renderPanel(

View File

@ -15,13 +15,6 @@ export const demoPanels: Panels = {
config: null,
url_path: "dev-state",
},
states: {
component_name: "states",
icon: null,
title: null,
config: null,
url_path: "states",
},
"dev-event": {
component_name: "dev-event",
icon: null,
@ -43,13 +36,6 @@ export const demoPanels: Panels = {
config: null,
url_path: "profile",
},
kiosk: {
component_name: "kiosk",
icon: null,
title: null,
config: null,
url_path: "kiosk",
},
"dev-info": {
component_name: "dev-info",
icon: null,

View File

@ -23,7 +23,7 @@ import { AppDrawerLayoutElement } from "@polymer/app-layout/app-drawer-layout/ap
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
import { toggleAttribute } from "../common/dom/toggle_attribute";
const NON_SWIPABLE_PANELS = ["kiosk", "map"];
const NON_SWIPABLE_PANELS = ["map"];
declare global {
// for fire event

View File

@ -9,7 +9,7 @@ import {
} from "./hass-router-page";
import { removeInitSkeleton } from "../util/init-skeleton";
const CACHE_URL_PATHS = ["lovelace", "states", "developer-tools"];
const CACHE_URL_PATHS = ["lovelace", "developer-tools"];
const COMPONENTS = {
calendar: () =>
import(
@ -31,10 +31,6 @@ const COMPONENTS = {
import(
/* webpackChunkName: "panel-lovelace" */ "../panels/lovelace/ha-panel-lovelace"
),
states: () =>
import(
/* webpackChunkName: "panel-states" */ "../panels/states/ha-panel-states"
),
history: () =>
import(
/* webpackChunkName: "panel-history" */ "../panels/history/ha-panel-history"
@ -43,10 +39,6 @@ const COMPONENTS = {
import(
/* webpackChunkName: "panel-iframe" */ "../panels/iframe/ha-panel-iframe"
),
kiosk: () =>
import(
/* webpackChunkName: "panel-kiosk" */ "../panels/kiosk/ha-panel-kiosk"
),
logbook: () =>
import(
/* webpackChunkName: "panel-logbook" */ "../panels/logbook/ha-panel-logbook"

View File

@ -78,85 +78,106 @@ export class DialogLovelaceDashboardDetail extends LitElement {
)}
>
<div>
${this._error
? html`
<div class="error">${this._error}</div>
`
: ""}
<div class="form">
<ha-switch
.checked=${this._showSidebar}
@change=${this._showSidebarChanged}
>${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.show_sidebar"
)}</ha-switch
>
${this._showSidebar
? html`
<ha-icon-input
.value=${this._sidebarIcon}
@value-changed=${this._sidebarIconChanged}
.label=${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.icon"
)}
></ha-icon-input>
<paper-input
.value=${this._sidebarTitle}
@value-changed=${this._sidebarTitleChanged}
.label=${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.title"
)}
@blur=${this._fillUrlPath}
></paper-input>
`
: ""}
${!this._params.dashboard
? html`
<paper-input
.value=${this._urlPath}
@value-changed=${this._urlChanged}
.label=${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.url"
)}
.errorMessage=${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.url_error_msg"
)}
.invalid=${urlInvalid}
></paper-input>
`
: ""}
<ha-switch
.checked=${this._requireAdmin}
@change=${this._requireAdminChanged}
>${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.require_admin"
)}</ha-switch
>
</div>
${this._params.dashboard && !this._params.dashboard.id
? this.hass!.localize(
"ui.panel.config.lovelace.dashboards.cant_edit_yaml"
)
: html`
${this._error
? html`
<div class="error">${this._error}</div>
`
: ""}
<div class="form">
<ha-switch
.checked=${this._showSidebar}
@change=${this._showSidebarChanged}
>${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.show_sidebar"
)}</ha-switch
>
${this._showSidebar
? html`
<ha-icon-input
.value=${this._sidebarIcon}
@value-changed=${this._sidebarIconChanged}
.label=${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.icon"
)}
></ha-icon-input>
<paper-input
.value=${this._sidebarTitle}
@value-changed=${this._sidebarTitleChanged}
.label=${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.title"
)}
@blur=${this._fillUrlPath}
></paper-input>
`
: ""}
${!this._params.dashboard
? html`
<paper-input
.value=${this._urlPath}
@value-changed=${this._urlChanged}
.label=${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.url"
)}
.errorMessage=${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.url_error_msg"
)}
.invalid=${urlInvalid}
></paper-input>
`
: ""}
<ha-switch
.checked=${this._requireAdmin}
@change=${this._requireAdminChanged}
>${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.require_admin"
)}</ha-switch
>
</div>
`}
</div>
${this._params.dashboard
? html`
<mwc-button
slot="secondaryAction"
class="warning"
@click="${this._deleteDashboard}"
.disabled=${this._submitting}
>
${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.delete"
)}
${this._params.dashboard.id
? html`
<mwc-button
slot="secondaryAction"
class="warning"
@click=${this._deleteDashboard}
.disabled=${this._submitting}
>
${this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.delete"
)}
</mwc-button>
`
: ""}
<mwc-button slot="secondaryAction" @click=${this._toggleDefault}>
${this._params.dashboard.url_path === localStorage.defaultPage
? this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.remove_default"
)
: this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.set_default"
)}
</mwc-button>
`
: html``}
: ""}
<mwc-button
slot="primaryAction"
@click="${this._updateDashboard}"
.disabled=${urlInvalid || this._submitting}
>
${this._params.dashboard
? this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.update"
)
? this._params.dashboard.id
? this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.update"
)
: this.hass!.localize("ui.common.close")
: this.hass!.localize(
"ui.panel.config.lovelace.dashboards.detail.create"
)}
@ -199,6 +220,19 @@ export class DialogLovelaceDashboardDetail extends LitElement {
this._requireAdmin = (ev.target as HaSwitch).checked;
}
private _toggleDefault() {
const urlPath = this._params?.dashboard?.url_path;
if (!urlPath) {
return;
}
if (urlPath === localStorage.defaultPage) {
delete localStorage.defaultPage;
} else {
localStorage.defaultPage = urlPath;
}
location.reload();
}
private async _updateDashboard() {
this._submitting = true;
try {

View File

@ -27,10 +27,7 @@ import {
} from "../../../../data/lovelace";
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
import { compare } from "../../../../common/string/compare";
import {
showConfirmationDialog,
showAlertDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { lovelaceTabs } from "../ha-config-lovelace";
import { navigate } from "../../../../common/navigate";
@ -164,6 +161,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
.columns=${this._columns(this.hass.language, this._dashboards)}
.data=${this._getItems(this._dashboards)}
@row-click=${this._editDashboard}
id="url_path"
>
</hass-tabs-subpage-data-table>
<ha-fab
@ -194,18 +192,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
}
private _editDashboard(ev: CustomEvent) {
const id = (ev.detail as RowClickedEvent).id;
const dashboard = id
? this._dashboards.find((res) => res.id === id)
: undefined;
if (!dashboard) {
showAlertDialog(this, {
text: this.hass!.localize(
"ui.panel.config.lovelace.dashboards.cant_edit_yaml"
),
});
return;
}
const urlPath = (ev.detail as RowClickedEvent).id;
const dashboard = this._dashboards.find((res) => res.url_path === urlPath);
this._openDialog(dashboard);
}

View File

@ -15,7 +15,6 @@ import "./integrations-card";
const JS_TYPE = __BUILD__;
const JS_VERSION = __VERSION__;
const OPT_IN_PANEL = "states";
class HaPanelDevInfo extends LitElement {
@property() public hass!: HomeAssistant;
@ -25,28 +24,6 @@ class HaPanelDevInfo extends LitElement {
const customUiList: Array<{ name: string; url: string; version: string }> =
(window as any).CUSTOM_UI_LIST || [];
const nonDefaultLink =
localStorage.defaultPage === OPT_IN_PANEL && OPT_IN_PANEL === "states"
? "/lovelace"
: "/states";
const nonDefaultLinkText =
localStorage.defaultPage === OPT_IN_PANEL && OPT_IN_PANEL === "states"
? this.hass.localize("ui.panel.developer-tools.tabs.info.lovelace_ui")
: `${this.hass.localize(
"ui.panel.developer-tools.tabs.info.states_ui"
)} (DEPRECATED)`;
const defaultPageText = `${this.hass.localize(
"ui.panel.developer-tools.tabs.info.default_ui",
"action",
localStorage.defaultPage === OPT_IN_PANEL
? this.hass.localize("ui.panel.developer-tools.tabs.info.remove")
: this.hass.localize("ui.panel.developer-tools.tabs.info.set"),
"name",
`${OPT_IN_PANEL} (DEPRECATED)`
)}`;
return html`
<div class="about">
<p class="version">
@ -142,11 +119,6 @@ class HaPanelDevInfo extends LitElement {
: ""
}
</p>
<p>
<a href="${nonDefaultLink}">${nonDefaultLinkText}</a><br />
<a href="#" @click="${this._toggleDefaultPage}">${defaultPageText}</a
><br />
</p>
</div>
<div class="content">
<system-health-card .hass=${this.hass}></system-health-card>
@ -167,15 +139,6 @@ class HaPanelDevInfo extends LitElement {
}, 1000);
}
protected _toggleDefaultPage(): void {
if (localStorage.defaultPage === OPT_IN_PANEL) {
delete localStorage.defaultPage;
} else {
localStorage.defaultPage = OPT_IN_PANEL;
}
this.requestUpdate();
}
static get styles(): CSSResult[] {
return [
haStyle,

View File

@ -1,27 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../states/ha-panel-states";
class HaPanelKiosk extends PolymerElement {
static get template() {
return html`
<ha-panel-states
id="kiosk-states"
hass="[[hass]]"
show-menu
route="[[route]]"
panel-visible
></ha-panel-states>
`;
}
static get properties() {
return {
hass: Object,
route: Object,
};
}
}
customElements.define("ha-panel-kiosk", HaPanelKiosk);

View File

@ -1,456 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-scroll-effects/effects/waterfall";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/app-route/app-route";
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/iron-pages/iron-pages";
import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import "@material/mwc-button/mwc-button";
import "../../components/ha-cards";
import "../../components/ha-icon";
import "../../components/ha-menu-button";
import "../../layouts/ha-app-layout";
import { extractViews } from "../../common/entity/extract_views";
import { getViewEntities } from "../../common/entity/get_view_entities";
import { computeStateName } from "../../common/entity/compute_state_name";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import computeLocationName from "../../common/config/location_name";
import NavigateMixin from "../../mixins/navigate-mixin";
import { EventsMixin } from "../../mixins/events-mixin";
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
const ALWAYS_SHOW_DOMAIN = ["persistent_notification", "configurator"];
/*
* @appliesMixin EventsMixin
* @appliesMixin NavigateMixin
*/
class PartialCards extends EventsMixin(NavigateMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex iron-positioning ha-style">
:host {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
ha-app-layout {
min-height: 100%;
background-color: var(--secondary-background-color, #e5e5e5);
}
iron-pages {
height: 100%;
}
paper-tabs {
margin-left: 12px;
--paper-tabs-selection-bar-color: var(--text-primary-color, #fff);
text-transform: uppercase;
}
mwc-button {
--mdc-theme-primary: var(--text-primary-color, #fff);
}
a {
text-decoration: none;
}
</style>
<app-route
route="{{route}}"
pattern="/:view"
data="{{routeData}}"
active="{{routeMatch}}"
></app-route>
<ha-app-layout id="layout">
<app-header effects="waterfall" condenses="" fixed="" slot="header">
<app-toolbar>
<ha-menu-button
hass="[[hass]]"
narrow="[[narrow]]"
></ha-menu-button>
<div main-title="">
[[computeTitle(views, defaultView, locationName)]]
</div>
<paper-icon-button
hidden$="[[!conversation]]"
aria-label="Start conversation"
icon="hass:microphone"
on-click="_showVoiceCommandDialog"
></paper-icon-button>
<a
href="https://github.com/home-assistant/home-assistant-polymer/issues/4459"
target="_blank"
><mwc-button outlined>DEPRECATED</mwc-button></a
>
</app-toolbar>
<div sticky="" hidden$="[[areTabsHidden(views, showTabs)]]">
<paper-tabs
scrollable=""
selected="[[currentView]]"
attr-for-selected="data-entity"
on-iron-activate="handleViewSelected"
>
<paper-tab data-entity="" on-click="scrollToTop">
<template is="dom-if" if="[[!defaultView]]">
Home
</template>
<template is="dom-if" if="[[defaultView]]">
<template is="dom-if" if="[[defaultView.attributes.icon]]">
<ha-icon
title$="[[_computeStateName(defaultView)]]"
icon="[[defaultView.attributes.icon]]"
></ha-icon>
</template>
<template is="dom-if" if="[[!defaultView.attributes.icon]]">
[[_computeStateName(defaultView)]]
</template>
</template>
</paper-tab>
<template is="dom-repeat" items="[[views]]">
<paper-tab
data-entity$="[[item.entity_id]]"
on-click="scrollToTop"
>
<template is="dom-if" if="[[item.attributes.icon]]">
<ha-icon
title$="[[_computeStateName(item)]]"
icon="[[item.attributes.icon]]"
></ha-icon>
</template>
<template is="dom-if" if="[[!item.attributes.icon]]">
[[_computeStateName(item)]]
</template>
</paper-tab>
</template>
</paper-tabs>
</div>
</app-header>
<iron-pages
attr-for-selected="data-view"
selected="[[currentView]]"
selected-attribute="view-visible"
>
<ha-cards
data-view=""
states="[[viewStates]]"
columns="[[_columns]]"
hass="[[hass]]"
panel-visible="[[panelVisible]]"
ordered-group-entities="[[orderedGroupEntities]]"
></ha-cards>
<template is="dom-repeat" items="[[views]]">
<ha-cards
data-view$="[[item.entity_id]]"
states="[[viewStates]]"
columns="[[_columns]]"
hass="[[hass]]"
panel-visible="[[panelVisible]]"
ordered-group-entities="[[orderedGroupEntities]]"
></ha-cards>
</template>
</iron-pages>
</ha-app-layout>
`;
}
static get properties() {
return {
hass: {
type: Object,
value: null,
observer: "hassChanged",
},
narrow: {
type: Boolean,
value: false,
},
panelVisible: {
type: Boolean,
value: false,
},
route: Object,
routeData: Object,
routeMatch: Boolean,
_columns: {
type: Number,
value: 1,
},
conversation: {
type: Boolean,
computed: "_computeConversation(hass)",
},
locationName: {
type: String,
value: "",
computed: "_computeLocationName(hass)",
},
currentView: {
type: String,
computed: "_computeCurrentView(hass, routeMatch, routeData)",
},
views: {
type: Array,
},
defaultView: {
type: Object,
},
viewStates: {
type: Object,
computed: "computeViewStates(currentView, hass, defaultView)",
},
orderedGroupEntities: {
type: Array,
computed: "computeOrderedGroupEntities(currentView, hass, defaultView)",
},
showTabs: {
type: Boolean,
value: true,
},
};
}
static get observers() {
return ["_updateColumns(narrow, hass.dockedSidebar)"];
}
ready() {
this._updateColumns = this._updateColumns.bind(this);
this.mqls = [300, 600, 900, 1200].map((width) =>
matchMedia(`(min-width: ${width}px)`)
);
super.ready();
}
connectedCallback() {
super.connectedCallback();
this.mqls.forEach((mql) => mql.addListener(this._updateColumns));
}
disconnectedCallback() {
super.disconnectedCallback();
this.mqls.forEach((mql) => mql.removeListener(this._updateColumns));
}
_updateColumns() {
const matchColumns = this.mqls.reduce((cols, mql) => cols + mql.matches, 0);
// Do -1 column if the menu is docked and open
this._columns = Math.max(
1,
matchColumns - (!this.narrow && this.hass.dockedSidebar === "docked")
);
}
_computeConversation(hass) {
return isComponentLoaded(hass, "conversation");
}
_showVoiceCommandDialog() {
showVoiceCommandDialog(this);
}
areTabsHidden(views, showTabs) {
return !views || !views.length || !showTabs;
}
/**
* Scroll to a specific y coordinate.
*
* Copied from paper-scroll-header-panel.
*
* @method scroll
* @param {number} top The coordinate to scroll to, along the y-axis.
* @param {boolean} smooth true if the scroll position should be smoothly adjusted.
*/
scrollToTop() {
// the scroll event will trigger _updateScrollState directly,
// However, _updateScrollState relies on the previous `scrollTop` to update the states.
// Calling _updateScrollState will ensure that the states are synced correctly.
var top = 0;
var scroller = this.$.layout.header.scrollTarget;
var easingFn = function easeOutQuad(t, b, c, d) {
/* eslint-disable no-param-reassign, space-infix-ops, no-mixed-operators */
t /= d;
return -c * t * (t - 2) + b;
/* eslint-enable no-param-reassign, space-infix-ops, no-mixed-operators */
};
var animationId = Math.random();
var duration = 200;
var startTime = Date.now();
var currentScrollTop = scroller.scrollTop;
var deltaScrollTop = top - currentScrollTop;
this._currentAnimationId = animationId;
(function updateFrame() {
var now = Date.now();
var elapsedTime = now - startTime;
if (elapsedTime > duration) {
scroller.scrollTop = top;
} else if (this._currentAnimationId === animationId) {
scroller.scrollTop = easingFn(
elapsedTime,
currentScrollTop,
deltaScrollTop,
duration
);
requestAnimationFrame(updateFrame.bind(this));
}
}.call(this));
}
handleViewSelected(ev) {
const view = ev.detail.item.getAttribute("data-entity") || null;
if (view !== this.currentView) {
let path = "/states";
if (view) {
path += "/" + view;
}
this.navigate(path);
}
}
_computeCurrentView(hass, routeMatch, routeData) {
if (!routeMatch) return "";
if (
!hass.states[routeData.view] ||
!hass.states[routeData.view].attributes.view
) {
return "";
}
return routeData.view;
}
computeTitle(views, defaultView, locationName) {
return (views &&
views.length > 0 &&
!defaultView &&
locationName === "Home") ||
!locationName
? "Home Assistant"
: locationName;
}
_computeStateName(stateObj) {
return computeStateName(stateObj);
}
_computeLocationName(hass) {
return computeLocationName(hass);
}
hassChanged(hass) {
if (!hass) return;
const views = extractViews(hass.states);
let defaultView = null;
// If default view present, it's in first index.
if (views.length > 0 && views[0].entity_id === DEFAULT_VIEW_ENTITY_ID) {
defaultView = views.shift();
}
this.setProperties({ views, defaultView });
}
isView(currentView, defaultView) {
return (
(currentView || defaultView) &&
this.hass.states[currentView || DEFAULT_VIEW_ENTITY_ID]
);
}
_defaultViewFilter(hass, entityId) {
// Filter out hidden
return !hass.states[entityId].attributes.hidden;
}
_computeDefaultViewStates(hass, entityIds) {
const states = {};
entityIds
.filter(this._defaultViewFilter.bind(null, hass))
.forEach((entityId) => {
states[entityId] = hass.states[entityId];
});
return states;
}
/*
Compute the states to show for current view.
Will make sure we always show entities from ALWAYS_SHOW_DOMAINS domains.
*/
computeViewStates(currentView, hass, defaultView) {
const entityIds = Object.keys(hass.states);
// If we base off all entities, only have to filter out hidden
if (!this.isView(currentView, defaultView)) {
return this._computeDefaultViewStates(hass, entityIds);
}
let states;
if (currentView) {
states = getViewEntities(hass.states, hass.states[currentView]);
} else {
states = getViewEntities(
hass.states,
hass.states[DEFAULT_VIEW_ENTITY_ID]
);
}
// Make sure certain domains are always shown.
entityIds.forEach((entityId) => {
const state = hass.states[entityId];
if (ALWAYS_SHOW_DOMAIN.includes(computeStateDomain(state))) {
states[entityId] = state;
}
});
return states;
}
/*
Compute the ordered list of groups for this view
*/
computeOrderedGroupEntities(currentView, hass, defaultView) {
if (!this.isView(currentView, defaultView)) {
return null;
}
var orderedGroupEntities = {};
var entitiesList =
hass.states[currentView || DEFAULT_VIEW_ENTITY_ID].attributes.entity_id;
for (var i = 0; i < entitiesList.length; i++) {
orderedGroupEntities[entitiesList[i]] = i;
}
return orderedGroupEntities;
}
}
customElements.define("ha-panel-states", PartialCards);

View File

@ -882,7 +882,9 @@
"require_admin": "Admin only",
"delete": "Delete",
"update": "Update",
"create": "Create"
"create": "Create",
"set_default": "Set as default on this device",
"remove_default": "Remove as default on this device"
}
},
"resources": {