Merge pull request #2208 from home-assistant/dev

20181207.0
This commit is contained in:
Paulus Schoutsen 2018-12-07 07:10:32 +01:00 committed by GitHub
commit 181539baac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 373 additions and 221 deletions

View File

@ -172,10 +172,14 @@ const CONFIGS = [
- type: glance
entities:
- entity: lock.kitchen_door
tap_action: toggle
tap_action:
type: toggle
- entity: light.ceiling_lights
tap_action: call-service
service: light.turn_on
tap_action:
action: call-service
service: light.turn_on
service_data:
entity_id: light.ceiling_lights
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun

View File

@ -56,7 +56,8 @@ const CONFIGS = [
--iron-icon-fill-color: rgba(50, 50, 50, .75)
- type: image
entity: light.bed_light
tap_action: toggle
tap_action:
action: toggle
image: /images/light_bulb_off.png
state_image:
'on': /images/light_bulb_on.png

View File

@ -83,6 +83,23 @@ const CONFIGS = [
- binary_sensor.basement_floor_wet
`,
},
{
heading: "Custom tap action",
config: `
- type: picture-glance
image: /images/living_room.png
title: Living room
entity: light.ceiling_lights
tap_action:
action: toggle
entities:
- entity: switch.decorative_lights
icon: mdi:power
tap_action:
action: toggle
- binary_sensor.basement_floor_wet
`,
},
];
class DemoPicGlance extends PolymerElement {

View File

@ -4,6 +4,8 @@
# Stop on errors
set -e
cd "$(dirname "$0")/.."
BUILD_DIR=build
OUTPUT_DIR=hass_frontend
OUTPUT_DIR_ES5=hass_frontend_es5

11
script/size_stats Executable file
View File

@ -0,0 +1,11 @@
#!/bin/sh
# Analyze stats
# Stop on errors
set -e
cd "$(dirname "$0")/.."
STATS=1 NODE_ENV=production webpack --profile --json > compilation-stats.json
npx webpack-bundle-analyzer compilation-stats.json hass_frontend
rm compilation-stats.json

View File

@ -1,7 +1,9 @@
import Leaflet from "leaflet";
// Sets up a Leaflet map on the provided DOM element
export default function setupLeafletMap(mapElement) {
export const setupLeafletMap = async (mapElement) => {
const Leaflet = (await import(/* webpackChunkName: "leaflet" */ "leaflet"))
.default;
Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
const map = Leaflet.map(mapElement);
const style = document.createElement("link");
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
@ -21,5 +23,5 @@ export default function setupLeafletMap(mapElement) {
}
).addTo(map);
return map;
}
return [map, Leaflet];
};

View File

@ -51,9 +51,13 @@ export type ActionConfig =
| MoreInfoActionConfig
| NoActionConfig;
export const fetchConfig = (hass: HomeAssistant): Promise<LovelaceConfig> =>
export const fetchConfig = (
hass: HomeAssistant,
force: boolean
): Promise<LovelaceConfig> =>
hass.callWS({
type: "lovelace/config",
force,
});
export const migrateConfig = (hass: HomeAssistant): Promise<void> =>

View File

@ -8,18 +8,18 @@ import { TemplateResult } from "lit-html";
import { fireEvent } from "../../../common/dom/fire_event";
import { styleMap } from "lit-html/directives/styleMap";
import { jQuery } from "../../../resources/jquery";
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
import { HomeAssistant, LightEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard } from "../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { longPress } from "../common/directives/long-press-directive";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { loadRoundslider } from "../../../resources/jquery.roundslider.ondemand";
import { toggleEntity } from "../common/entity/toggle-entity";
import stateIcon from "../../../common/entity/state_icon";
import computeStateName from "../../../common/entity/compute_state_name";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../../../components/ha-card";
import "../../../components/ha-icon";
@ -49,11 +49,15 @@ export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
public hass?: HomeAssistant;
private _config?: Config;
private _brightnessTimout?: number;
private _roundSliderStyle?: TemplateResult;
private _jQuery?: any;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
roundSliderStyle: {},
_jQuery: {},
};
}
@ -99,14 +103,14 @@ export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
color: this._computeColor(stateObj),
})
}"
@ha-click="${() => this._handleClick(false)}"
@ha-hold="${() => this._handleClick(true)}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
></ha-icon>
<div
class="brightness"
@ha-click="${() => this._handleClick(false)}"
@ha-hold="${() => this._handleClick(true)}"
@ha-click="${this._handleTap}"
@ha-hold="${this._handleHold}"
.longPress="${longPress()}"
></div>
<div class="name">
@ -124,10 +128,15 @@ export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
return hasConfigOrEntityChanged(this, changedProps);
}
protected firstUpdated(): void {
protected async firstUpdated(): Promise<void> {
const loaded = await loadRoundslider();
this._roundSliderStyle = loaded.roundSliderStyle;
this._jQuery = loaded.jQuery;
const brightness = this.hass!.states[this._config!.entity].attributes
.brightness;
jQuery("#light", this.shadowRoot).roundSlider({
this._jQuery("#light", this.shadowRoot).roundSlider({
...lightConfig,
change: (value) => this._setBrightness(value),
drag: (value) => this._dragEvent(value),
@ -139,13 +148,13 @@ export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
}
protected updated(changedProps: PropertyValues): void {
if (!this._config || !this.hass) {
if (!this._config || !this.hass || !this._jQuery) {
return;
}
const attrs = this.hass!.states[this._config!.entity].attributes;
jQuery("#light", this.shadowRoot).roundSlider({
this._jQuery("#light", this.shadowRoot).roundSlider({
value: Math.round((attrs.brightness / 254) * 100) || 0,
});
@ -157,7 +166,7 @@ export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
private renderStyle(): TemplateResult {
return html`
${roundSliderStyle}
${this._roundSliderStyle}
<style>
:host {
display: block;
@ -311,18 +320,13 @@ export class HuiLightCard extends hassLocalizeLitMixin(LitElement)
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
}
private _handleClick(hold: boolean): void {
const entityId = this._config!.entity;
private _handleTap() {
toggleEntity(this.hass!, this._config!.entity!);
}
if (hold) {
fireEvent(this, "hass-more-info", {
entityId,
});
return;
}
this.hass!.callService("light", "toggle", {
entity_id: entityId,
private _handleHold() {
fireEvent(this, "hass-more-info", {
entityId: this._config!.entity,
});
}
}

View File

@ -1,19 +1,16 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@polymer/paper-icon-button/paper-icon-button";
import Leaflet from "leaflet";
import "../../map/ha-entity-marker";
import setupLeafletMap from "../../../common/dom/setup-leaflet-map";
import { setupLeafletMap } from "../../../common/dom/setup-leaflet-map";
import { processConfigEntities } from "../common/process-config-entities";
import computeStateDomain from "../../../common/entity/compute_state_domain";
import computeStateName from "../../../common/entity/compute_state_name";
import debounce from "../../../common/util/debounce";
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
class HuiMapCard extends PolymerElement {
static get template() {
return html`
@ -143,13 +140,14 @@ class HuiMapCard extends PolymerElement {
window.addEventListener("resize", this._debouncedResizeListener);
}
this._map = setupLeafletMap(this.$.map);
this._drawEntities(this.hass);
this.loadMap();
}
setTimeout(() => {
this._resetMap();
this._fitMap();
}, 1);
async loadMap() {
[this._map, this.Leaflet] = await setupLeafletMap(this.$.map);
this._drawEntities(this.hass);
this._map.invalidateSize();
this._fitMap();
}
disconnectedCallback() {
@ -177,7 +175,7 @@ class HuiMapCard extends PolymerElement {
const zoom = this._config.default_zoom;
if (this._mapItems.length === 0) {
this._map.setView(
new Leaflet.LatLng(
new this.Leaflet.LatLng(
this.hass.config.latitude,
this.hass.config.longitude
),
@ -186,7 +184,7 @@ class HuiMapCard extends PolymerElement {
return;
}
const bounds = new Leaflet.latLngBounds(
const bounds = new this.Leaflet.latLngBounds(
this._mapItems.map((item) => item.getLatLng())
);
this._map.fitBounds(bounds.pad(0.5));
@ -245,7 +243,7 @@ class HuiMapCard extends PolymerElement {
iconHTML = title;
}
markerIcon = Leaflet.divIcon({
markerIcon = this.Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className: "",
@ -253,7 +251,7 @@ class HuiMapCard extends PolymerElement {
// create market with the icon
mapItems.push(
Leaflet.marker([latitude, longitude], {
this.Leaflet.marker([latitude, longitude], {
icon: markerIcon,
interactive: false,
title: title,
@ -262,7 +260,7 @@ class HuiMapCard extends PolymerElement {
// create circle around it
mapItems.push(
Leaflet.circle([latitude, longitude], {
this.Leaflet.circle([latitude, longitude], {
interactive: false,
color: "#FF9800",
radius: radius,
@ -285,9 +283,9 @@ class HuiMapCard extends PolymerElement {
el.setAttribute("entity-name", entityName);
el.setAttribute("entity-picture", entityPicture || "");
/* Leaflet clones this element before adding it to the map. This messes up
/* this.Leaflet clones this element before adding it to the map. This messes up
our Polymer object and we can't pass data through. Thus we hack like this. */
markerIcon = Leaflet.divIcon({
markerIcon = this.Leaflet.divIcon({
html: el.outerHTML,
iconSize: [48, 48],
className: "",
@ -295,7 +293,7 @@ class HuiMapCard extends PolymerElement {
// create market with the icon
mapItems.push(
Leaflet.marker([latitude, longitude], {
this.Leaflet.marker([latitude, longitude], {
icon: markerIcon,
title: computeStateName(stateObj),
}).addTo(map)
@ -304,7 +302,7 @@ class HuiMapCard extends PolymerElement {
// create circle around if entity has accuracy
if (gpsAccuracy) {
mapItems.push(
Leaflet.circle([latitude, longitude], {
this.Leaflet.circle([latitude, longitude], {
interactive: false,
color: "#0288D1",
radius: gpsAccuracy,

View File

@ -71,11 +71,13 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
.entity="${this._config.entity}"
.aspectRatio="${this._config.aspect_ratio}"
></hui-image>
${
this._config.elements.map((elementConfig: LovelaceElementConfig) =>
this._createHuiElement(elementConfig)
)
}
<div id="root">
${
this._config.elements.map((elementConfig: LovelaceElementConfig) =>
this._createHuiElement(elementConfig)
)
}
</div>
</ha-card>
`;
}

View File

@ -6,13 +6,11 @@ import {
} from "@polymer/lit-element";
import { classMap } from "lit-html/directives/classMap";
import { TemplateResult } from "lit-html";
import { jQuery } from "../../../resources/jquery";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import computeStateName from "../../../common/entity/compute_state_name";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { roundSliderStyle } from "../../../resources/jquery.roundslider";
import { HomeAssistant, ClimateEntity } from "../../../types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard } from "../types";
@ -20,6 +18,7 @@ import { LovelaceCardConfig } from "../../../data/lovelace";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import { loadRoundslider } from "../../../resources/jquery.roundslider.ondemand";
const thermostatConfig = {
radius: 150,
@ -58,11 +57,15 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public hass?: HomeAssistant;
private _config?: Config;
private _roundSliderStyle?: TemplateResult;
private _jQuery?: any;
static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
roundSliderStyle: {},
_jQuery: {},
};
}
@ -134,7 +137,12 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
return hasConfigOrEntityChanged(this, changedProps);
}
protected firstUpdated(): void {
protected async firstUpdated(): Promise<void> {
const loaded = await loadRoundslider();
this._roundSliderStyle = loaded.roundSliderStyle;
this._jQuery = loaded.jQuery;
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
const _sliderType =
@ -143,7 +151,7 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
? "range"
: "min-range";
jQuery("#thermostat", this.shadowRoot).roundSlider({
this._jQuery("#thermostat", this.shadowRoot).roundSlider({
...thermostatConfig,
radius: this.clientWidth / 3,
min: stateObj.attributes.min_temp,
@ -155,7 +163,7 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
}
protected updated(changedProps: PropertyValues): void {
if (!this._config || !this.hass) {
if (!this._config || !this.hass || !this._jQuery) {
return;
}
@ -179,7 +187,7 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
sliderValue = uiValue = stateObj.attributes.temperature;
}
jQuery("#thermostat", this.shadowRoot).roundSlider({
this._jQuery("#thermostat", this.shadowRoot).roundSlider({
value: sliderValue,
});
this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = uiValue;
@ -192,7 +200,7 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
private renderStyle(): TemplateResult {
return html`
${roundSliderStyle}
${this._roundSliderStyle}
<style>
:host {
display: block;

View File

@ -1,4 +1,4 @@
import "../../../cards/ha-camera-card";
import "../../../cards/ha-weather-card";
import LegacyWrapperCard from "./hui-legacy-wrapper-card";

View File

@ -1,6 +1,7 @@
import computeStateName from "../../../common/entity/compute_state_name";
import { HomeAssistant } from "../../../types";
import { LovelaceElementConfig } from "../elements/types";
import { ActionConfig } from "../../../data/lovelace";
export const computeTooltip = (
hass: HomeAssistant,
@ -11,7 +12,7 @@ export const computeTooltip = (
}
let stateName = "";
let tooltip: string;
let tooltip = "";
if (config.entity) {
stateName =
@ -20,19 +21,45 @@ export const computeTooltip = (
: config.entity;
}
switch (config.tap_action && config.tap_action.action) {
case "navigate":
tooltip = `Navigate to ${config.navigation_path}`;
break;
case "toggle":
tooltip = `Toggle ${stateName}`;
break;
case "call-service":
tooltip = `Call service ${config.service}`;
break;
default:
tooltip = `Show more-info: ${stateName}`;
}
const tapTooltip = config.tap_action
? computeActionTooltip(stateName, config.tap_action, false)
: "";
const holdTooltip = config.hold_action
? computeActionTooltip(stateName, config.hold_action, true)
: "";
const newline = tapTooltip && holdTooltip ? "\n" : "";
tooltip = tapTooltip + newline + holdTooltip;
return tooltip;
};
function computeActionTooltip(
state: string,
config: ActionConfig,
isHold: boolean
) {
if (!config || !config.action || config.action === "none") {
return "";
}
let tooltip = isHold ? "Hold: " : "Tap: ";
switch (config.action) {
case "navigate":
tooltip += `Navigate to ${config.navigation_path}`;
break;
case "toggle":
tooltip += `Toggle ${state}`;
break;
case "call-service":
tooltip += `Call service ${config.service}`;
break;
case "more-info":
tooltip += `Show more-info: ${state}`;
break;
}
return tooltip;
}

View File

@ -1,7 +1,7 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "@polymer/paper-button/paper-button";
import { fireEvent } from "../../../common/dom/fire_event";
import { showEditCardDialog } from "../editor/hui-dialog-edit-card";
import { showEditCardDialog } from "../editor/show-edit-card-dialog";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { confDeleteCard } from "../editor/delete-card";
@ -50,14 +50,15 @@ export class HuiCardOptions extends hassLocalizeLitMixin(LitElement) {
</style>
<slot></slot>
<div>
<paper-button @click="${this._editCard}"
>${
this.localize("ui.panel.lovelace.editor.edit_card.edit")
}</paper-button
>
<paper-button class="warning" @click="${this._deleteCard}"
>${
this.localize("ui.panel.lovelace.editor.edit_card.delete")
}</paper-button
><paper-button @click="${this._editCard}"
>${
this.localize("ui.panel.lovelace.editor.edit_card.edit")
}</paper-button
>
</div>
`;

View File

@ -1,5 +1,4 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import "@polymer/paper-button/paper-button";
import { TemplateResult } from "lit-html";
import { HomeAssistant } from "../../../types";
@ -84,9 +83,6 @@ export class HuiEntityEditor extends LitElement {
.entities {
padding-left: 20px;
}
paper-button {
margin: 8px 0;
}
</style>
`;
}

View File

@ -28,7 +28,8 @@ export class HuiViewEditor extends hassLocalizeLitMixin(LitElement) {
if (!this._config) {
return "";
}
return this._config.id || "";
return "id" in this._config ? this._config.id! : "";
}
get _title(): string {

View File

@ -2,7 +2,7 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { HomeAssistant } from "../../../types";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { LovelaceCardConfig } from "../../../data/lovelace";
import "./hui-edit-card";
import "./hui-migrate-config";
@ -11,7 +11,6 @@ declare global {
// for fire event
interface HASSDomEvents {
"reload-lovelace": undefined;
"show-edit-card": EditCardDialogParams;
}
// for add event listener
interface HTMLElementEventMap {
@ -19,10 +18,6 @@ declare global {
}
}
let registeredDialog = false;
const dialogShowEvent = "show-edit-card";
const dialogTag = "hui-dialog-edit-card";
export interface EditCardDialogParams {
cardConfig?: LovelaceCardConfig;
viewId?: string | number;
@ -30,24 +25,6 @@ export interface EditCardDialogParams {
reloadLovelace: () => void;
}
const registerEditCardDialog = (element: HTMLElement) =>
fireEvent(element, "register-dialog", {
dialogShowEvent,
dialogTag,
dialogImport: () => import("./hui-dialog-edit-card"),
});
export const showEditCardDialog = (
element: HTMLElement,
editCardDialogParams: EditCardDialogParams
) => {
if (!registeredDialog) {
registeredDialog = true;
registerEditCardDialog(element);
}
fireEvent(element, dialogShowEvent, editCardDialogParams);
};
export class HuiDialogEditCard extends LitElement {
protected hass?: HomeAssistant;
private _params?: EditCardDialogParams;
@ -110,4 +87,4 @@ declare global {
}
}
customElements.define(dialogTag, HuiDialogEditCard);
customElements.define("hui-dialog-edit-card", HuiDialogEditCard);

View File

@ -2,16 +2,15 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { HomeAssistant } from "../../../types";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import { LovelaceViewConfig } from "../../../data/lovelace";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import "./hui-edit-view";
import "./hui-migrate-config";
import { EditViewDialogParams } from "./show-edit-view-dialog";
declare global {
// for fire event
interface HASSDomEvents {
"reload-lovelace": undefined;
"show-edit-view": EditViewDialogParams;
}
// for add event listener
interface HTMLElementEventMap {
@ -19,34 +18,6 @@ declare global {
}
}
let registeredDialog = false;
const dialogShowEvent = "show-edit-view";
const dialogTag = "hui-dialog-edit-view";
export interface EditViewDialogParams {
viewConfig?: LovelaceViewConfig;
add?: boolean;
reloadLovelace: () => void;
}
const registerEditViewDialog = (element: HTMLElement) =>
fireEvent(element, "register-dialog", {
dialogShowEvent,
dialogTag,
dialogImport: () => import("./hui-dialog-edit-view"),
});
export const showEditViewDialog = (
element: HTMLElement,
editViewDialogParams: EditViewDialogParams
) => {
if (!registeredDialog) {
registeredDialog = true;
registerEditViewDialog(element);
}
fireEvent(element, dialogShowEvent, editViewDialogParams);
};
export class HuiDialogEditView extends LitElement {
protected hass?: HomeAssistant;
private _params?: EditViewDialogParams;
@ -71,7 +42,7 @@ export class HuiDialogEditView extends LitElement {
if (
!this._params.add &&
this._params.viewConfig &&
!this._params.viewConfig.id
!("id" in this._params.viewConfig)
) {
return html`
<hui-migrate-config
@ -98,4 +69,4 @@ declare global {
}
}
customElements.define(dialogTag, HuiDialogEditView);
customElements.define("hui-dialog-edit-view", HuiDialogEditView);

View File

@ -10,6 +10,10 @@ import "@polymer/paper-spinner/paper-spinner";
import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-icon-button/paper-icon-button.js";
import "@polymer/paper-item/paper-item.js";
import "@polymer/paper-listbox/paper-listbox.js";
import "@polymer/paper-menu-button/paper-menu-button.js";
// This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
@ -28,6 +32,8 @@ import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { EntitiesEditorEvent, ViewEditEvent } from "./types";
import { processEditorEntities } from "./process-editor-entities";
import { EntityConfig } from "../entity-rows/types";
import { confDeleteView } from "./delete-view";
import { navigate } from "../../../common/navigate";
export class HuiEditView extends hassLocalizeLitMixin(LitElement) {
static get properties(): PropertyDeclarations {
@ -79,7 +85,7 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) {
) {
const { cards, badges, ...viewConfig } = this.viewConfig;
this._config = viewConfig;
this._badges = processEditorEntities(badges);
this._badges = badges ? processEditorEntities(badges) : [];
} else if (changedProperties.has("add")) {
this._config = {};
this._badges = [];
@ -146,6 +152,15 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) {
></paper-spinner>
${this.localize("ui.common.save")}</paper-button
>
<paper-menu-button no-animations>
<paper-icon-button
icon="hass:dots-vertical"
slot="dropdown-trigger"
></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-item @click="${this._delete}">Delete</paper-item>
</paper-listbox>
</paper-menu-button>
</div>
</paper-dialog>
`;
@ -189,6 +204,20 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) {
this._updateConfigInBackend();
}
private _delete() {
if (this._config!.cards && this._config!.cards!.length > 0) {
alert(
"You can't delete a view that has card in them. Remove the cards first."
);
return;
}
confDeleteView(this.hass!, this._config!.id!, () => {
this._closeDialog();
this.reloadLovelace!();
navigate(this, `/lovelace/0`);
});
}
private async _resizeDialog(): Promise<void> {
await this.updateComplete;
fireEvent(this._dialog, "iron-resize");
@ -233,7 +262,7 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) {
} else {
await updateViewConfig(
this.hass!,
this.viewConfig!.id!,
String(this.viewConfig!.id!),
this._config,
"json"
);

View File

@ -0,0 +1,38 @@
import { LovelaceCardConfig } from "../../../data/lovelace";
import { fireEvent } from "../../../common/dom/fire_event";
declare global {
// for fire event
interface HASSDomEvents {
"show-edit-card": EditCardDialogParams;
}
}
let registeredDialog = false;
const dialogShowEvent = "show-edit-card";
const dialogTag = "hui-dialog-edit-card";
export interface EditCardDialogParams {
cardConfig?: LovelaceCardConfig;
viewId?: string | number;
add: boolean;
reloadLovelace: () => void;
}
const registerEditCardDialog = (element: HTMLElement) =>
fireEvent(element, "register-dialog", {
dialogShowEvent,
dialogTag,
dialogImport: () => import("./hui-dialog-edit-card"),
});
export const showEditCardDialog = (
element: HTMLElement,
editCardDialogParams: EditCardDialogParams
) => {
if (!registeredDialog) {
registeredDialog = true;
registerEditCardDialog(element);
}
fireEvent(element, dialogShowEvent, editCardDialogParams);
};

View File

@ -0,0 +1,42 @@
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
import { LovelaceViewConfig } from "../../../data/lovelace";
declare global {
// for fire event
interface HASSDomEvents {
"reload-lovelace": undefined;
"show-edit-view": EditViewDialogParams;
}
// for add event listener
interface HTMLElementEventMap {
"reload-lovelace": HASSDomEvent<undefined>;
}
}
let registeredDialog = false;
const dialogShowEvent = "show-edit-view";
const dialogTag = "hui-dialog-edit-view";
export interface EditViewDialogParams {
viewConfig?: LovelaceViewConfig;
add?: boolean;
reloadLovelace: () => void;
}
const registerEditViewDialog = (element: HTMLElement) =>
fireEvent(element, "register-dialog", {
dialogShowEvent,
dialogTag,
dialogImport: () => import("./hui-dialog-edit-view"),
});
export const showEditViewDialog = (
element: HTMLElement,
editViewDialogParams: EditViewDialogParams
) => {
if (!registeredDialog) {
registeredDialog = true;
registerEditViewDialog(element);
}
fireEvent(element, dialogShowEvent, editViewDialogParams);
};

View File

@ -8,7 +8,6 @@ export interface LovelaceElementConfig {
hold_action?: ActionConfig;
service?: string;
service_data?: object;
navigation_path?: string;
tap_action?: ActionConfig;
title?: string;
}

View File

@ -8,11 +8,15 @@ import "./hui-error-entity-row";
import { HomeAssistant } from "../../../types";
import { EntityRow, EntityConfig } from "./types";
import computeStateDisplay from "../../../common/entity/compute_state_display";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
interface SensorEntityConfig extends EntityConfig {
format?: "relative" | "date" | "time" | "datetime";
}
class HuiSensorEntityRow extends LitElement implements EntityRow {
class HuiSensorEntityRow extends hassLocalizeLitMixin(LitElement)
implements EntityRow {
public hass?: HomeAssistant;
private _config?: SensorEntityConfig;
@ -58,7 +62,7 @@ class HuiSensorEntityRow extends LitElement implements EntityRow {
.format="${this._config.format}"
></hui-timestamp-display>
`
: stateObj.state
: computeStateDisplay(this.localize, stateObj, this.hass.language)
}
</div>
</hui-generic-entity-row>

View File

@ -28,7 +28,7 @@ class Lovelace extends localizeMixin(PolymerElement) {
route="[[route]]"
config="[[_config]]"
columns="[[_columns]]"
on-config-refresh="_fetchConfig"
on-config-refresh="_forceFetchConfig"
></hui-root>
</template>
<template
@ -48,7 +48,7 @@ class Lovelace extends localizeMixin(PolymerElement) {
narrow="[[narrow]]"
show-menu="[[showMenu]]"
>
<paper-button on-click="_fetchConfig"
<paper-button on-click="_forceFetchConfig"
>Reload ui-lovelace.yaml</paper-button
>
</hass-error-screen>
@ -96,7 +96,7 @@ class Lovelace extends localizeMixin(PolymerElement) {
}
ready() {
this._fetchConfig();
this._fetchConfig(false);
this._updateColumns = this._updateColumns.bind(this);
this.mqls = [300, 600, 900, 1200].map((width) => {
const mql = matchMedia(`(min-width: ${width}px)`);
@ -113,9 +113,13 @@ class Lovelace extends localizeMixin(PolymerElement) {
this._columns = Math.max(1, matchColumns - (!this.narrow && this.showMenu));
}
async _fetchConfig() {
_forceFetchConfig() {
this._fetchConfig(true);
}
async _fetchConfig(force) {
try {
const conf = await fetchConfig(this.hass);
const conf = await fetchConfig(this.hass, force);
this.setProperties({
_config: conf,
_state: "loaded",

View File

@ -33,8 +33,7 @@ import "./hui-view";
import debounce from "../../common/util/debounce";
import createCardElement from "./common/create-card-element";
import { showSaveDialog } from "./editor/hui-dialog-save-config";
import { showEditViewDialog } from "./editor/hui-dialog-edit-view";
import { confDeleteView } from "./editor/delete-view";
import { showEditViewDialog } from "./editor/show-edit-view-dialog";
// CSS and JS should only be imported once. Modules and HTML are safe.
const CSS_CACHE = {};
@ -61,7 +60,6 @@ class HUIRoot extends NavigateMixin(
text-transform: uppercase;
}
#add-view {
background: var(--paper-fab-background, var(--accent-color));
position: absolute;
height: 44px;
}
@ -71,12 +69,9 @@ class HUIRoot extends NavigateMixin(
paper-button.warning:not([disabled]) {
color: var(--google-red-500);
}
app-toolbar.secondary {
background-color: var(--light-primary-color);
color: var(--primary-text-color, #333);
font-size: 14px;
font-weight: 500;
height: auto;
#add-view ha-icon {
background-color: var(--accent-color);
border-radius: 5px;
}
#view {
min-height: calc(100vh - 112px);
@ -95,6 +90,9 @@ class HUIRoot extends NavigateMixin(
paper-item {
cursor: pointer;
}
.edit-view-icon {
padding-left: 8px;
}
</style>
<app-route route="[[route]]" pattern="/:view" data="{{routeData}}"></app-route>
<hui-notification-drawer
@ -140,7 +138,7 @@ class HUIRoot extends NavigateMixin(
</app-toolbar>
</template>
<div sticky hidden$="[[_computeTabsHidden(config.views)]]">
<div sticky hidden$="[[_computeTabsHidden(config.views, _editMode)]]">
<paper-tabs scrollable selected="[[_curView]]" on-iron-activate="_handleViewSelected">
<template is="dom-repeat" items="[[config.views]]">
<paper-tab>
@ -150,6 +148,9 @@ class HUIRoot extends NavigateMixin(
<template is="dom-if" if="[[!item.icon]]">
[[_computeTabTitle(item.title)]]
</template>
<template is='dom-if' if="[[_computeEditVisible(_editMode, config.views)]]">
<ha-icon class="edit-view-icon" on-click="_editView" icon="hass:pencil"></ha-icon>
</template>
</paper-tab>
</template>
<template is='dom-if' if="[[_editMode]]">
@ -160,12 +161,6 @@ class HUIRoot extends NavigateMixin(
</paper-tabs>
</div>
</app-header>
<template is='dom-if' if="[[_editMode]]">
<app-toolbar class="secondary">
<paper-button on-click="_editView">[[localize("ui.panel.lovelace.editor.edit_view.edit")]]</paper-button>
<paper-button class="warning" on-click="_deleteView">[[localize("ui.panel.lovelace.editor.edit_view.delete")]]</paper-button>
</app-toolbar>
</template>
<div id='view' on-rebuild-view='_debouncedConfigChanged'></div>
</app-header-layout>
`;
@ -280,8 +275,8 @@ class HUIRoot extends NavigateMixin(
return config.title || "Home Assistant";
}
_computeTabsHidden(views) {
return views.length < 2;
_computeTabsHidden(views, editMode) {
return views.length < 2 && !editMode;
}
_computeTabTitle(title) {
@ -304,6 +299,10 @@ class HUIRoot extends NavigateMixin(
window.open("https://www.home-assistant.io/lovelace/", "_blank");
}
_computeEditVisible(editMode, views) {
return editMode && views[this._curView];
}
_editModeEnable() {
if (this.config._frontendAuto) {
showSaveDialog(this, {
@ -316,10 +315,18 @@ class HUIRoot extends NavigateMixin(
return;
}
this._editMode = true;
if (this.config.views.length < 2) {
this.$.view.classList.remove("tabs-hidden");
this.fire("iron-resize");
}
}
_editModeDisable() {
this._editMode = false;
if (this.config.views.length < 2) {
this.$.view.classList.add("tabs-hidden");
this.fire("iron-resize");
}
}
_editModeChanged() {
@ -345,24 +352,6 @@ class HUIRoot extends NavigateMixin(
});
}
_deleteView() {
const viewConfig = this.config.views[this._curView];
if (viewConfig.cards && viewConfig.cards.length > 0) {
alert(
"You can't delete a view that has card in them. Remove the cards first."
);
return;
}
if (!viewConfig.id) {
this._editView();
return;
}
confDeleteView(this.hass, viewConfig.id, () => {
this.fire("config-refresh");
this._navigateView(0);
});
}
_handleViewSelected(ev) {
const index = ev.detail.selected;
this._navigateView(index);
@ -393,6 +382,10 @@ class HUIRoot extends NavigateMixin(
view.setConfig(this.config);
} else {
const viewConfig = this.config.views[this._curView];
if (!viewConfig) {
this._editModeEnable();
return;
}
if (viewConfig.panel) {
view = createCardElement(viewConfig.cards[0]);
view.isPanel = true;

View File

@ -11,7 +11,7 @@ import EventsMixin from "../../mixins/events-mixin";
import localizeMixin from "../../mixins/localize-mixin";
import createCardElement from "./common/create-card-element";
import { computeCardSize } from "./common/compute-card-size";
import { showEditCardDialog } from "./editor/hui-dialog-edit-card";
import { showEditCardDialog } from "./editor/show-edit-card-dialog";
class HUIView extends localizeMixin(EventsMixin(PolymerElement)) {
static get template() {
@ -119,7 +119,7 @@ class HUIView extends localizeMixin(EventsMixin(PolymerElement)) {
_addCard() {
showEditCardDialog(this, {
viewId: this.config.id,
viewId: "id" in this.config ? String(this.config.id) : undefined,
add: true,
reloadLovelace: () => {
this.fire("config-refresh");

View File

@ -1,7 +1,6 @@
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import Leaflet from "leaflet";
import "../../components/ha-menu-button";
import "../../components/ha-icon";
@ -11,9 +10,7 @@ import "./ha-entity-marker";
import computeStateDomain from "../../common/entity/compute_state_domain";
import computeStateName from "../../common/entity/compute_state_name";
import LocalizeMixin from "../../mixins/localize-mixin";
import setupLeafletMap from "../../common/dom/setup-leaflet-map";
Leaflet.Icon.Default.imagePath = "/static/images/leaflet";
import { setupLeafletMap } from "../../common/dom/setup-leaflet-map";
/*
* @appliesMixin LocalizeMixin
@ -61,14 +58,14 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
connectedCallback() {
super.connectedCallback();
var map = (this._map = setupLeafletMap(this.$.map));
this.loadMap();
}
async loadMap() {
[this._map, this.Leaflet] = await setupLeafletMap(this.$.map);
this.drawEntities(this.hass);
setTimeout(() => {
map.invalidateSize();
this.fitMap();
}, 1);
this._map.invalidateSize();
this.fitMap();
}
disconnectedCallback() {
@ -82,14 +79,14 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
if (this._mapItems.length === 0) {
this._map.setView(
new Leaflet.LatLng(
new this.Leaflet.LatLng(
this.hass.config.latitude,
this.hass.config.longitude
),
14
);
} else {
bounds = new Leaflet.latLngBounds(
bounds = new this.Leaflet.latLngBounds(
this._mapItems.map((item) => item.getLatLng())
);
this._map.fitBounds(bounds.pad(0.5));
@ -108,7 +105,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
}
var mapItems = (this._mapItems = []);
Object.keys(hass.states).forEach(function(entityId) {
Object.keys(hass.states).forEach((entityId) => {
var entity = hass.states[entityId];
var title = computeStateName(entity);
@ -137,7 +134,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
iconHTML = title;
}
icon = Leaflet.divIcon({
icon = this.Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className: "",
@ -145,7 +142,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
// create market with the icon
mapItems.push(
Leaflet.marker(
this.Leaflet.marker(
[entity.attributes.latitude, entity.attributes.longitude],
{
icon: icon,
@ -157,7 +154,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
// create circle around it
mapItems.push(
Leaflet.circle(
this.Leaflet.circle(
[entity.attributes.latitude, entity.attributes.longitude],
{
interactive: false,
@ -181,7 +178,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
.join("");
/* Leaflet clones this element before adding it to the map. This messes up
our Polymer object and we can't pass data through. Thus we hack like this. */
icon = Leaflet.divIcon({
icon = this.Leaflet.divIcon({
html:
"<ha-entity-marker entity-id='" +
entity.entity_id +
@ -196,7 +193,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
// create market with the icon
mapItems.push(
Leaflet.marker(
this.Leaflet.marker(
[entity.attributes.latitude, entity.attributes.longitude],
{
icon: icon,
@ -208,7 +205,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
// create circle around if entity has accuracy
if (entity.attributes.gps_accuracy) {
mapItems.push(
Leaflet.circle(
this.Leaflet.circle(
[entity.attributes.latitude, entity.attributes.longitude],
{
interactive: false,

View File

@ -1,8 +1,12 @@
import { html } from "@polymer/lit-element";
import "./jquery";
// jQuery import should come before plugin import
import { jQuery as jQuery_ } from "./jquery";
import "round-slider";
// eslint-disable-next-line
import roundSliderCSS from "round-slider/dist/roundslider.min.css";
export const jQuery = jQuery_;
export const roundSliderStyle = html`
<style>
${roundSliderCSS}

View File

@ -0,0 +1,15 @@
import { TemplateResult } from "lit-html";
type LoadedRoundSlider = Promise<{
roundSliderStyle: TemplateResult;
jQuery: any;
}>;
let loaded: LoadedRoundSlider;
export const loadRoundslider = async (): LoadedRoundSlider => {
if (!loaded) {
loaded = import(/* webpackChunkName: "jquery-roundslider" */ "./jquery.roundslider");
}
return loaded;
};

View File

@ -16,6 +16,7 @@ if (!version) {
}
const VERSION = version[0];
const isCI = process.env.CI === "true";
const isStatsBuild = process.env.STATS === "1";
const generateJSPage = (entrypoint, latestBuild) => {
return new HtmlWebpackPlugin({
@ -51,10 +52,6 @@ function createConfig(isProdBuild, latestBuild) {
entry["service-worker-hass"] = "./src/entrypoints/service-worker-hass.js";
}
const chunkFilename = isProdBuild
? "[chunkhash].chunk.js"
: "[name].chunk.js";
return {
mode: isProdBuild ? "production" : "development",
devtool: isProdBuild
@ -161,6 +158,7 @@ function createConfig(isProdBuild, latestBuild) {
),
isProdBuild &&
!isCI &&
!isStatsBuild &&
new CompressionPlugin({
cache: true,
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
@ -223,7 +221,10 @@ function createConfig(isProdBuild, latestBuild) {
if (!isProdBuild || dontHash.has(chunk.name)) return `${chunk.name}.js`;
return `${chunk.name}-${chunk.hash.substr(0, 8)}.js`;
},
chunkFilename: chunkFilename,
chunkFilename:
isProdBuild && !isStatsBuild
? "[chunkhash].chunk.js"
: "[name].chunk.js",
path: path.resolve(__dirname, buildPath),
publicPath,
},
@ -243,7 +244,7 @@ function createConfig(isProdBuild, latestBuild) {
const isProdBuild = process.env.NODE_ENV === "production";
const configs = [createConfig(isProdBuild, /* latestBuild */ true)];
if (isProdBuild) {
if (isProdBuild && !isStatsBuild) {
configs.push(createConfig(isProdBuild, /* latestBuild */ false));
}
module.exports = configs;