Lovelace cleanups (#3427)

* Improvements

* Add types CAF

* Fix demo switching

* Do not set background color in hui-view
This commit is contained in:
Paulus Schoutsen 2019-07-26 11:06:16 -07:00 committed by GitHub
parent dae0ecce6a
commit 6abbe72e4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 233 additions and 107 deletions

View File

@ -1,4 +1,4 @@
// Run HA develop mode
// Run demo develop mode
const gulp = require("gulp");
require("./clean.js");

View File

@ -84,12 +84,12 @@ gulp.task("webpack-dev-server-demo", () => {
open: true,
watchContentBase: true,
contentBase: path.resolve(paths.demo_dir, "dist"),
}).listen(8080, "localhost", function(err) {
}).listen(8090, "localhost", function(err) {
if (err) {
throw err;
}
// Server listening
log("[webpack-dev-server]", "http://localhost:8080");
log("[webpack-dev-server]", "http://localhost:8090");
});
});

View File

@ -96,7 +96,7 @@ const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
// Create an object mapping browser urls to their paths during build
const translationMetadata = require("../build-translations/translationMetadata.json");
const workBoxTranslationsTemplatedURLs = {};
const englishFP = translationMetadata["translations"]["en"]["fingerprints"];
const englishFP = translationMetadata.translations.en.fingerprints;
Object.keys(englishFP).forEach((key) => {
workBoxTranslationsTemplatedURLs[
`/static/translations/${englishFP[key]}`
@ -192,7 +192,7 @@ const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => {
new webpack.DefinePlugin({
__DEV__: !isProdBuild,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify("DEMO"),
__VERSION__: JSON.stringify(`DEMO-${version}`),
__DEMO__: true,
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(

View File

@ -16,6 +16,7 @@ import { mockEvents } from "./stubs/events";
import { mockMediaPlayer } from "./stubs/media_player";
import { HomeAssistant } from "../../src/types";
import { mockFrontend } from "./stubs/frontend";
import { mockPersistentNotification } from "./stubs/persistent_notification";
class HaDemo extends HomeAssistantAppEl {
protected async _initialize() {
@ -43,6 +44,7 @@ class HaDemo extends HomeAssistantAppEl {
mockEvents(hass);
mockMediaPlayer(hass);
mockFrontend(hass);
mockPersistentNotification(hass);
// Once config is loaded AND localize, set entities and apply theme.
Promise.all([selectedDemoConfig, localizePromise]).then(

View File

@ -0,0 +1,16 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { PersistentNotification } from "../../../src/data/persistent_notification";
export const mockPersistentNotification = (hass: MockHomeAssistant) => {
hass.mockWS("persistent_notification/get", () =>
Promise.resolve([
{
created_at: new Date().toISOString(),
message: "There was motion detected in the backyard.",
notification_id: "demo-1",
title: "Motion Detected!",
status: "unread",
},
] as PersistentNotification[])
);
};

View File

@ -78,7 +78,7 @@
"fuse.js": "^3.4.4",
"google-timezones-json": "^1.0.2",
"hls.js": "^0.12.4",
"home-assistant-js-websocket": "^4.2.2",
"home-assistant-js-websocket": "4.3.1",
"intl-messageformat": "^2.2.0",
"jquery": "^3.3.1",
"js-yaml": "^3.13.0",
@ -112,6 +112,8 @@
"@babel/preset-typescript": "^7.3.3",
"@gfx/zopfli": "^1.0.11",
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^3.0.12",
"@types/chromecast-caf-sender": "^1.0.1",
"@types/hls.js": "^0.12.3",
"@types/leaflet": "^1.4.3",
"@types/memoize-one": "4.1.0",

View File

@ -199,3 +199,9 @@ class HaEntityPicker extends LitElement {
}
customElements.define("ha-entity-picker", HaEntityPicker);
declare global {
interface HTMLElementTagNameMap {
"ha-entity-picker": HaEntityPicker;
}
}

View File

@ -15,8 +15,10 @@ import { HassEntity } from "home-assistant-js-websocket";
// Not duplicate, this is for typing.
// tslint:disable-next-line
import { HaIcon } from "../ha-icon";
import { HomeAssistant } from "../../types";
class StateBadge extends LitElement {
public hass?: HomeAssistant;
@property() public stateObj?: HassEntity;
@property() public overrideIcon?: string;
@query("ha-icon") private _icon!: HaIcon;
@ -54,8 +56,11 @@ class StateBadge extends LitElement {
if (stateObj) {
// hide icon if we have entity picture
if (stateObj.attributes.entity_picture && !this.overrideIcon) {
hostStyle.backgroundImage =
"url(" + stateObj.attributes.entity_picture + ")";
let imageUrl = stateObj.attributes.entity_picture;
if (this.hass) {
imageUrl = this.hass.hassUrl(imageUrl);
}
hostStyle.backgroundImage = `url(${imageUrl})`;
iconStyle.display = "none";
} else {
if (stateObj.attributes.hs_color) {

View File

@ -35,8 +35,13 @@ export const fetchThumbnailUrlWithCache = (
entityId
);
export const fetchThumbnailUrl = (hass: HomeAssistant, entityId: string) =>
getSignedPath(hass, `/api/camera_proxy/${entityId}`).then(({ path }) => path);
export const fetchThumbnailUrl = async (
hass: HomeAssistant,
entityId: string
) => {
const path = await getSignedPath(hass, `/api/camera_proxy/${entityId}`);
return hass.hassUrl(path.path);
};
export const fetchThumbnail = (hass: HomeAssistant, entityId: string) => {
// tslint:disable-next-line: no-console
@ -47,7 +52,7 @@ export const fetchThumbnail = (hass: HomeAssistant, entityId: string) => {
});
};
export const fetchStreamUrl = (
export const fetchStreamUrl = async (
hass: HomeAssistant,
entityId: string,
format?: "hls"
@ -60,7 +65,9 @@ export const fetchStreamUrl = (
// @ts-ignore
data.format = format;
}
return hass.callWS<Stream>(data);
const stream = await hass.callWS<Stream>(data);
stream.url = hass.hassUrl(stream.url);
return stream;
};
export const fetchCameraPrefs = (hass: HomeAssistant, entityId: string) =>

View File

@ -1,5 +1,5 @@
import { HomeAssistant } from "../types";
import { Connection } from "home-assistant-js-websocket";
import { Connection, getCollection } from "home-assistant-js-websocket";
export interface LovelaceConfig {
title?: string;
@ -83,6 +83,17 @@ export const subscribeLovelaceUpdates = (
onChange: () => void
) => conn.subscribeEvents(onChange, "lovelace_updated");
export const getLovelaceCollection = (conn: Connection) =>
getCollection(
conn,
"_lovelace",
(conn2) => fetchConfig(conn2, false),
(_conn, store) =>
subscribeLovelaceUpdates(conn, () =>
fetchConfig(conn, false).then((config) => store.setState(config, true))
)
);
export interface WindowWithLovelaceProm extends Window {
llConfProm?: Promise<LovelaceConfig>;
}

View File

@ -203,6 +203,25 @@ class CoverEntity extends Entity {
}
}
class InputNumberEntity extends Entity {
public async handleService(
domain,
service,
// @ts-ignore
data
) {
if (domain !== this.domain) {
return;
}
if (service === "set_value") {
this.update("" + data.value);
} else {
super.handleService(domain, service, data);
}
}
}
class ClimateEntity extends Entity {
public async handleService(domain, service, data) {
if (domain !== this.domain) {
@ -256,6 +275,7 @@ const TYPES = {
cover: CoverEntity,
group: GroupEntity,
input_boolean: ToggleEntity,
input_number: InputNumberEntity,
light: LightEntity,
lock: LockEntity,
media_player: MediaPlayerEntity,

View File

@ -90,7 +90,11 @@ export const provideHass = (
const hassObj: MockHomeAssistant = {
// Home Assistant properties
auth: {} as any,
auth: {
data: {
hassUrl: "",
},
} as any,
connection: {
addEventListener: () => undefined,
removeEventListener: () => undefined,
@ -182,6 +186,7 @@ export const provideHass = (
? response[1](hass(), method, path, parameters)
: Promise.reject(`API Mock for ${path} is not implemented`);
},
hassUrl: (path?) => path,
fetchWithAuth: () => Promise.reject("Not implemented"),
sendWS: (msg) => hassObj.connection.sendMessage(msg),
callWS: (msg) => hassObj.connection.sendMessagePromise(msg),

View File

@ -1,15 +1,4 @@
<meta charset="utf-8">
<link rel='manifest' href='/manifest.json' crossorigin="use-credentials">
<link rel='icon' href='/static/icons/favicon.ico'>
<meta name='viewport' content='width=device-width, user-scalable=no'>
<style>
body {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-weight: 400;
margin: 0;
padding: 0;
height: 100vh;
}
</style>
<%= renderTemplate('_style_base') %>

View File

@ -0,0 +1,12 @@
<meta name='viewport' content='width=device-width, user-scalable=no'>
<style>
body {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-weight: 400;
margin: 0;
padding: 0;
height: 100vh;
}
</style>

View File

@ -198,8 +198,9 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
${this._config!.show_icon !== false
? html`
<state-badge
.stateObj="${stateObj}"
.overrideIcon="${entityConf.icon}"
.hass=${this.hass}
.stateObj=${stateObj}
.overrideIcon=${entityConf.icon}
></state-badge>
`
: ""}

View File

@ -148,6 +148,27 @@ class HuiMapCard extends LitElement implements LovelaceCard {
`;
}
protected shouldUpdate(changedProps) {
if (!changedProps.has("hass") || changedProps.size > 1) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || !this._configEntities) {
return true;
}
// Check if any state has changed
for (const entity of this._configEntities) {
if (oldHass.states[entity.entity] !== this.hass!.states[entity.entity]) {
return true;
}
}
return false;
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this.loadMap();

View File

@ -59,15 +59,6 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
static get styles(): CSSResult {
return css`
:host {
/* start paper-font-body1 style */
font-family: "Roboto", "Noto", sans-serif;
-webkit-font-smoothing: antialiased; /* OS X subpixel AA bleed bug */
font-size: 14px;
font-weight: 400;
line-height: 20px;
/* end paper-font-body1 style */
}
ha-markdown {
display: block;
padding: 0 16px 16px;

View File

@ -64,7 +64,7 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
),
})}"
>
<img src="${this._config.image}" />
<img src="${this.hass.hassUrl(this._config.image)}" />
</ha-card>
`;
}

View File

@ -492,7 +492,8 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
}
#thermostat .rs-handle {
background-color: var(--paper-card-background-color, white);
padding: 7px;
padding: 10px;
margin: -10px 0 0 -8px !important;
border: 2px solid var(--disabled-text-color);
}
#thermostat .rs-handle.rs-focus {

View File

@ -30,6 +30,7 @@ export interface EntitiesCardEntityConfig extends EntityConfig {
}
export interface EntitiesCardConfig extends LovelaceCardConfig {
type: "entities";
show_header_toggle?: boolean;
title?: string;
entities: EntitiesCardEntityConfig[];
@ -104,6 +105,7 @@ export interface LightCardConfig extends LovelaceCardConfig {
}
export interface MapCardConfig extends LovelaceCardConfig {
type: "map";
title: string;
aspect_ratio: string;
default_zoom?: number;
@ -113,6 +115,7 @@ export interface MapCardConfig extends LovelaceCardConfig {
}
export interface MarkdownCardConfig extends LovelaceCardConfig {
type: "markdown";
content: string;
title?: string;
}

View File

@ -129,6 +129,10 @@ export const createRowElement = (
return element;
}
if (!config.entity) {
return _createErrorElement("Invalid config given.", config);
}
const domain = config.entity.split(".", 1)[0];
tag = `hui-${DOMAIN_TO_ELEMENT_TYPE[domain] || "text"}-entity-row`;

View File

@ -0,0 +1,44 @@
import { loadModule, loadCSS, loadJS } from "../../../common/dom/load_resource";
import { LovelaceConfig } from "../../../data/lovelace";
// CSS and JS should only be imported once. Modules and HTML are safe.
const CSS_CACHE = {};
const JS_CACHE = {};
export const loadLovelaceResources = (
resources: NonNullable<LovelaceConfig["resources"]>,
hassUrl: string
) =>
resources.forEach((resource) => {
const normalizedUrl = new URL(resource.url, hassUrl).toString();
switch (resource.type) {
case "css":
if (normalizedUrl in CSS_CACHE) {
break;
}
CSS_CACHE[normalizedUrl] = loadCSS(normalizedUrl);
break;
case "js":
if (normalizedUrl in JS_CACHE) {
break;
}
JS_CACHE[normalizedUrl] = loadJS(normalizedUrl);
break;
case "module":
loadModule(normalizedUrl);
break;
case "html":
import(/* webpackChunkName: "import-href-polyfill" */ "../../../resources/html-import/import-href").then(
({ importHref }) => importHref(normalizedUrl)
);
break;
default:
// tslint:disable-next-line
console.warn(`Unknown resource type specified: ${resource.type}`);
}
});

View File

@ -47,8 +47,9 @@ class HuiGenericEntityRow extends LitElement {
return html`
<state-badge
.stateObj="${stateObj}"
.overrideIcon="${this.config.icon}"
.hass=${this.hass}
.stateObj=${stateObj}
.overrideIcon=${this.config.icon}
></state-badge>
<div class="flex">
<div class="info">
@ -63,8 +64,8 @@ class HuiGenericEntityRow extends LitElement {
: this.config.secondary_info === "last-changed"
? html`
<ha-relative-time
.hass="${this.hass}"
.datetime="${stateObj.last_changed}"
.hass=${this.hass}
.datetime=${stateObj.last_changed}
></ha-relative-time>
`
: ""}

View File

@ -104,6 +104,10 @@ export class HuiImage extends LitElement {
imageSrc = this.image;
}
if (imageSrc) {
imageSrc = this.hass!.hassUrl(imageSrc);
}
// Figure out filter to use
let filter = this.filter || "";

View File

@ -7,20 +7,21 @@ export interface EntityConfig {
icon?: string;
}
export interface DividerConfig {
type: string;
type: "divider";
style: string;
}
export interface SectionConfig {
type: string;
type: "section";
label: string;
}
export interface WeblinkConfig {
type: string;
type: "weblink";
name?: string;
icon?: string;
url: string;
}
export interface CallServiceConfig extends EntityConfig {
type: "call-service";
action_name?: string;
service: string;
service_data?: { [key: string]: any };

View File

@ -28,7 +28,6 @@ import "../../components/ha-start-voice-button";
import "../../components/ha-paper-icon-button-arrow-next";
import "../../components/ha-paper-icon-button-arrow-prev";
import "../../components/ha-icon";
import { loadModule, loadCSS, loadJS } from "../../common/dom/load_resource";
import { debounce } from "../../common/util/debounce";
import { HomeAssistant } from "../../types";
import { LovelaceConfig } from "../../data/lovelace";
@ -47,10 +46,7 @@ import { Lovelace } from "./types";
import { afterNextRender } from "../../common/util/render-status";
import { haStyle } from "../../resources/styles";
import { computeRTLDirection } from "../../common/util/compute_rtl";
// CSS and JS should only be imported once. Modules and HTML are safe.
const CSS_CACHE = {};
const JS_CACHE = {};
import { loadLovelaceResources } from "./common/load-resources";
class HUIRoot extends LitElement {
@property() public hass?: HomeAssistant;
@ -349,10 +345,14 @@ class HUIRoot extends LitElement {
* https://www.w3.org/TR/CSS2/visudet.html#the-height-property
*/
position: relative;
display: flex;
}
#view.tabs-hidden {
min-height: calc(100vh - 64px);
}
#view > * {
flex: 1;
}
paper-item {
cursor: pointer;
}
@ -408,7 +408,12 @@ class HUIRoot extends LitElement {
| undefined;
if (!oldLovelace || oldLovelace.config !== this.lovelace!.config) {
this._loadResources(this.lovelace!.config.resources || []);
if (this.lovelace!.config.resources) {
loadLovelaceResources(
this.lovelace!.config.resources,
this.hass!.auth.data.hassUrl
);
}
// On config change, recreate the current view from scratch.
force = true;
// Recalculate to see if we need to adjust content area for tab bar
@ -595,40 +600,6 @@ class HUIRoot extends LitElement {
viewConfig.background || this.config.background || "";
root.append(view);
}
private _loadResources(resources) {
resources.forEach((resource) => {
switch (resource.type) {
case "css":
if (resource.url in CSS_CACHE) {
break;
}
CSS_CACHE[resource.url] = loadCSS(resource.url);
break;
case "js":
if (resource.url in JS_CACHE) {
break;
}
JS_CACHE[resource.url] = loadJS(resource.url);
break;
case "module":
loadModule(resource.url);
break;
case "html":
import(/* webpackChunkName: "import-href-polyfill" */ "../../resources/html-import/import-href").then(
({ importHref }) => importHref(resource.url)
);
break;
default:
// tslint:disable-next-line
console.warn(`Unknown resource type specified: ${resource.type}`);
}
});
}
}
declare global {

View File

@ -57,8 +57,8 @@ export class HUIView extends LitElement {
return {
hass: {},
lovelace: {},
columns: {},
index: {},
columns: { type: Number },
index: { type: Number },
_cards: {},
_badges: {},
};
@ -116,10 +116,10 @@ export class HUIView extends LitElement {
<style>
:host {
display: block;
box-sizing: border-box;
padding: 4px 4px 0;
transform: translateZ(0);
position: relative;
min-height: calc(100vh - 155px);
}
#badges {
@ -194,7 +194,9 @@ export class HUIView extends LitElement {
let editModeChanged = false;
let configChanged = false;
if (changedProperties.has("lovelace")) {
if (changedProperties.has("index")) {
configChanged = true;
} else if (changedProperties.has("lovelace")) {
const oldLovelace = changedProperties.get("lovelace") as Lovelace;
editModeChanged =
!oldLovelace || lovelace.editMode !== oldLovelace.editMode;
@ -310,10 +312,8 @@ export class HUIView extends LitElement {
this._cards = elements;
if ("theme" in config) {
applyThemesOnElement(root, this.hass!.themes, config.theme);
}
}
private _rebuildCard(
cardElToReplace: LovelaceCard,

View File

@ -46,6 +46,7 @@ export const connectionMixin = (
translationMetadata,
dockedSidebar: "docked",
moreInfoEntityId: null,
hassUrl: (path = "") => new URL(path, auth.data.hassUrl).toString(),
callService: async (domain, service, serviceData = {}) => {
if (__DEV__) {
// tslint:disable-next-line: no-console

View File

@ -142,22 +142,20 @@ export interface HomeAssistant {
dockedSidebar: "docked" | "always_hidden" | "auto";
moreInfoEntityId: string | null;
user?: CurrentUser;
callService: (
hassUrl(path?): string;
callService(
domain: string,
service: string,
serviceData?: { [key: string]: any }
) => Promise<void>;
callApi: <T>(
): Promise<void>;
callApi<T>(
method: "GET" | "POST" | "PUT" | "DELETE",
path: string,
parameters?: { [key: string]: any }
) => Promise<T>;
fetchWithAuth: (
path: string,
init?: { [key: string]: any }
) => Promise<Response>;
sendWS: (msg: MessageBase) => void;
callWS: <T>(msg: MessageBase) => Promise<T>;
): Promise<T>;
fetchWithAuth(path: string, init?: { [key: string]: any }): Promise<Response>;
sendWS(msg: MessageBase): void;
callWS<T>(msg: MessageBase): Promise<T>;
}
export type LightEntity = HassEntityBase & {

View File

@ -1570,6 +1570,16 @@
resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-0.4.31.tgz#a31d74241a6b1edbb973cf36d97a2896834a51f9"
integrity sha1-ox10JBprHtu5c8822XooloNKUfk=
"@types/chromecast-caf-receiver@^3.0.12":
version "3.0.12"
resolved "https://registry.yarnpkg.com/@types/chromecast-caf-receiver/-/chromecast-caf-receiver-3.0.12.tgz#0172edc5e43a0b4f426b21a614a58e04e3df009d"
integrity sha512-GdR9nGOENDWYhF40FasB0Xnsy3c+e68K90sGVBZx1W1N3LP1NGOmCtaxgUpxk4IuHYmzGrW7I57zWZIbT3D5BQ==
"@types/chromecast-caf-sender@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/chromecast-caf-sender/-/chromecast-caf-sender-1.0.1.tgz#da0047c41c2a7ecf2d5348715b27c4542ed9b579"
integrity sha512-/JuG+zrS+KCPwEiOrK9O7WrIMyiUEF7Ev9ywbzXcCOPkXin9tLX7w9zxCmxtnOPdgH9lZbtOvgo5IA4cEJknRg==
"@types/clean-css@*":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@types/clean-css/-/clean-css-4.2.1.tgz#cb0134241ec5e6ede1b5344bc829668fd9871a8d"
@ -7303,10 +7313,10 @@ hoek@6.x.x:
resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c"
integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==
home-assistant-js-websocket@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-4.2.2.tgz#e13b058a9e200bc56080e1b48fdeaaf1ed2e4e5f"
integrity sha512-4mXYbn2DCiDVBYGZROUSWLBDerSoDRJulw1GiQbhKEyrDhzFs5KQkcLdIu6k3CSDYQiiKQez5uAhOfb0Hr/M0A==
home-assistant-js-websocket@4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-4.3.1.tgz#be320672b070cb4fcae2f1709a0eba1845ef7b31"
integrity sha512-eVIRdisSmcIzYKNSgB3gqUCrZpQkSUKlluYTsM0NqpUc4W0hHmF2vd8bShl3URWJXPOI5XPdeHuAPPqc0gUj+Q==
homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1:
version "1.0.3"