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:
parent
1d1688093a
commit
7e48b21767
|
@ -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
|
||||
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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
|
||||
);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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": {
|
||||
|
|
Loading…
Reference in New Issue