* Add demo

* Fix stuff

* Lint

* Typescript and demo card

* More fixes

* Allow switching through configs

* Lint

* Lint2

* Add two demo configs

* Lint

* Lint
This commit is contained in:
Paulus Schoutsen 2019-01-18 21:24:32 -08:00 committed by GitHub
parent 65359aabe3
commit bb71fe0bec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 26510 additions and 1087 deletions

View File

@ -18,6 +18,7 @@
},
"globals": {
"__DEV__": false,
"__DEMO__": false,
"__BUILD__": false,
"__VERSION__": false,
"__STATIC_PATH__": false,

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

23
demo/public/index.html Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#2157BC" />
<title>Home Assistant Demo</title>
<script src="./custom-elements-es5-adapter.js"></script>
<script src="./compatibility.js"></script>
<script src="./main.js" async></script>
<style>
body {
font-family: Roboto, Noto, sans-serif;
margin: 0;
padding: 0;
}
</style>
</head>
<body></body>
</html>

17
demo/script/build_demo Executable file
View File

@ -0,0 +1,17 @@
#!/bin/sh
# Build the demo
# Stop on errors
set -e
cd "$(dirname "$0")/.."
OUTPUT_DIR=dist
rm -rf $OUTPUT_DIR
cd ..
DEMO=1 ./node_modules/.bin/gulp build-translations gen-icons
cd demo
NODE_ENV=production ../node_modules/.bin/webpack -p --config webpack.config.js

13
demo/script/develop_demo Executable file
View File

@ -0,0 +1,13 @@
#!/bin/sh
# Develop the demo
# Stop on errors
set -e
cd "$(dirname "$0")/.."
cd ..
DEMO=1 ./node_modules/.bin/gulp build-translations gen-icons
cd demo
../node_modules/.bin/webpack-dev-server

6
demo/src/auth.ts Normal file
View File

@ -0,0 +1,6 @@
import { MockHomeAssistant } from "../../src/fake_data/provide_hass";
export const mockAuth = (hass: MockHomeAssistant) => {
hass.mockWS("config/auth/list", () => []);
hass.mockWS("auth/refresh_tokens", () => []);
};

View File

@ -0,0 +1,25 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { Lovelace } from "../../../src/panels/lovelace/types";
import { DemoConfig } from "./types";
export const demoConfigs: Array<() => Promise<DemoConfig>> = [
() => import("./kernehed").then((mod) => mod.demoKernehed),
() => import("./jimpower").then((mod) => mod.demoJimpower),
];
export let selectedDemoConfigIndex: number = 0;
export let selectedDemoConfig: Promise<DemoConfig> = demoConfigs[
selectedDemoConfigIndex
]();
export const setDemoConfig = async (
hass: MockHomeAssistant,
lovelace: Lovelace,
index: number
) => {
selectedDemoConfigIndex = index;
selectedDemoConfig = demoConfigs[index]();
const config = await selectedDemoConfig;
hass.addEntities(config.entities(), true);
lovelace.saveConfig(config.lovelace());
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
import { DemoConfig } from "../types";
import { demoLovelaceJimpower } from "./lovelace";
import { demoEntitiesJimpower } from "./entities";
export const demoJimpower: DemoConfig = {
authorName: "Jimpower",
authorUrl: " https://github.com/JamesMcCarthy79/Home-Assistant-Config",
name: "Kingia Castle",
lovelace: demoLovelaceJimpower,
entities: demoEntitiesJimpower,
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
import { DemoConfig } from "../types";
import { demoLovelaceKernehed } from "./lovelace";
import { demoEntitiesKernehed } from "./entities";
export const demoKernehed: DemoConfig = {
authorName: "Kernehed",
authorUrl: "",
name: "Hem",
lovelace: demoLovelaceKernehed,
entities: demoEntitiesKernehed,
};

View File

@ -0,0 +1,492 @@
import { LovelaceConfig } from "../../../../src/data/lovelace";
export const demoLovelaceKernehed: () => LovelaceConfig = () => ({
name: "Hem",
resources: [
// {
// url: "/local/custom-lovelace/monster-card.js",
// type: "js",
// },
// {
// url: "/local/custom-lovelace/mini-media-player-bundle.js?v=0.9.8",
// type: "module",
// },
// {
// url: "/local/custom-lovelace/slideshow-card.js?=1.1.0",
// type: "js",
// },
// {
// url: "/local/custom-lovelace/fold-entity-row.js?v=3ae2c4",
// type: "js",
// },
// {
// url: "/local/custom-lovelace/swipe-card/swipe-card.js?v=2.0.0",
// type: "module",
// },
// {
// url: "/local/custom-lovelace/upcoming-media-card/upcoming-media-card.js",
// type: "js",
// },
// {
// url: "/local/custom-lovelace/tracker-card.js?v=0.1.5",
// type: "js",
// },
// {
// url: "/local/custom-lovelace/card-tools.js?v=6ce5d0",
// type: "js",
// },
// {
// url: "/local/custom-lovelace/krisinfo.js?=0.0.1",
// type: "js",
// },
],
views: [
{
cards: [
{ type: "custom:ha-demo-card" },
{
cards: [
{
cards: [
{
image: "/assets/kernehed/oscar.jpg",
elements: [
{
style: {
color: "white",
top: "93%",
left: "20%",
},
type: "state-label",
entity: "sensor.oskar_devices",
},
{
style: {
color: "white",
top: "93%",
left: "90%",
},
type: "state-label",
entity: "sensor.battery_oskar",
},
{
style: {
color: "white",
top: "93%",
left: "55%",
},
type: "state-label",
entity: "sensor.oskar_tid_till_hem",
},
],
type: "picture-elements",
},
{
image: "/assets/kernehed/bella.jpg",
elements: [
{
style: {
color: "white",
top: "92%",
left: "20%",
},
type: "state-label",
entity: "sensor.bella_devices",
},
{
style: {
color: "white",
top: "92%",
left: "90%",
},
type: "state-label",
entity: "sensor.battery_bella",
},
{
style: {
color: "white",
top: "92%",
left: "55%",
},
type: "state-label",
entity: "sensor.bella_tid_till_hem",
},
],
type: "picture-elements",
},
],
type: "horizontal-stack",
},
],
type: "vertical-stack",
id: "4db5c4664f0a4458949aec3651e4d7a6",
},
{
entities: [
"lock.polycontrol_danalock_v3_btze_locked",
"sensor.zwave_battery_front_door",
"alarm_control_panel.kernehed_manison",
"binary_sensor.dorrklockan",
],
show_header_toggle: false,
type: "entities",
id: "37279816181f442eac853b03c0473101",
title: "L\u00e5set",
},
// {
// filter: {
// exclude: [
// {
// state: "not_home",
// },
// ],
// include: [
// {
// entity_id: "device_tracker.annasiphone",
// },
// {
// entity_id: "device_tracker.iphone_2",
// },
// ],
// },
// type: "custom:monster-card",
// id: "6d4744d14a7c42668633cedbe655ba08",
// card: {
// show_header_toggle: false,
// type: "entities",
// title: "G\u00e4ster",
// },
// show_empty: false,
// },
// {
// filter: {
// exclude: [
// {
// state: "Inget",
// },
// {
// state: "i.u.",
// },
// ],
// include: [
// {
// entity_id: "sensor.pollen_al",
// },
// {
// entity_id: "sensor.pollen_alm",
// },
// {
// entity_id: "sensor.pollen_salg_vide",
// },
// {
// entity_id: "sensor.pollen_bjork",
// },
// {
// entity_id: "sensor.pollen_bok",
// },
// {
// entity_id: "sensor.pollen_ek",
// },
// {
// entity_id: "sensor.pollen_grabo",
// },
// {
// entity_id: "sensor.pollen_gras",
// },
// {
// entity_id: "sensor.pollen_hassel",
// },
// ],
// },
// type: "custom:monster-card",
// id: "7ecee83212d340b0901f63ac9ec24328",
// card: {
// show_header_toggle: false,
// type: "entities",
// title: "Pollenniv\u00e5er",
// },
// show_empty: false,
// },
{
cards: [
{
entities: [
"switch.rest_julbelysning",
"binary_sensor.front_door_sensor",
"binary_sensor.unifi_camera",
"binary_sensor.back_door_sensor",
],
image: "/assets/kernehed/camera.entre.jpg",
type: "picture-glance",
title: "Entr\u00e9 kamera",
},
{
entities: [
"input_select.christmas_pattern",
"input_select.christmas_palette",
],
type: "entities",
},
],
type: "vertical-stack",
id: "fc8abcaade0e4087a10a5602f3bdb4d4",
},
// {
// url: "https://embed.windy.com/embed2.html",
// type: "iframe",
// id: "3870fdc794274f17b84dd6ced631b737",
// },
{
entities: [
{
name: "Tv\u00e4ttstugan",
entity: "binary_sensor.tvattstugan_motion_sensor",
},
{
name: "Skafferiet",
entity: "binary_sensor.skafferiet_motion_sensor",
},
{
name: "K\u00e4llaren",
entity: "binary_sensor.kallaren_motion_sensor",
},
{
name: "Trappen",
entity: "binary_sensor.trapp_motion_sensor",
},
{
name: "B\u00e4nksensor",
entity: "binary_sensor.banksensor",
},
{
name: "Altansensor",
entity: "binary_sensor.altan_motion_sensor",
},
{
name: "Badrum",
entity: "binary_sensor.badrumssensor",
},
],
type: "glance",
id: "fac4c51ac1914e3a897da643077e15f3",
show_state: false,
},
{
entities: ["sensor.oskar_bluetooth"],
show_header_toggle: false,
type: "entities",
id: "37279816181f442eac853b132142141",
title: "Rum lokalisering",
},
// {
// filter: {
// exclude: [
// {
// state: false,
// },
// ],
// include: [
// {
// entity_id:
// "binary_sensor.fibaro_system_unknown_type0c02_id1003_sensor_2",
// },
// {
// entity_id:
// "binary_sensor.fibaro_system_unknown_type0c02_id1003_sensor_3",
// },
// ],
// },
// type: "custom:monster-card",
// id: "2a440c2701824fdb9d5ebc9827c0917b",
// card: {
// show_header_toggle: false,
// type: "entities",
// title: "Brandvarnare",
// },
// show_empty: false,
// },
{
type: "weather-forecast",
id: "2bf8ccbc1f664c23b10b6533ae82f7e2",
entity: "weather.smhi_vader",
},
// {
// cards: [
// {
// max: 50,
// min: -50,
// type: "gauge",
// title: "\u00d6verv\u00e5ning",
// entity:
// "sensor.fibaro_system_unknown_type0c02_id1003_temperature",
// },
// {
// max: 50,
// min: -50,
// type: "gauge",
// title: "Entr\u00e9n",
// entity:
// "sensor.fibaro_system_unknown_type0c02_id1003_temperature_2",
// },
// {
// max: 50,
// min: -50,
// type: "gauge",
// title: "K\u00e4llaren",
// entity:
// "sensor.philio_technology_corporation_phpat02beu_multisensor_2in1_temperature",
// },
// ],
// type: "custom:slideshow-card",
// arrow_color: "var(--primary-text-color)",
// arrow_opacity: 0.7,
// },
],
title: "Hem",
id: "hem",
icon: "mdi:home",
},
{
cards: [
{
entities: [
"sensor.processor_use",
"sensor.memory_free",
"sensor.disk_free_home",
"sensor.last_boot",
"sensor.db_size",
],
show_header_toggle: false,
type: "entities",
id: "7c92cd52219548b6a6a6d5ee6088e071",
title: "System",
},
{
entities: [
"sensor.pi_hole_dns_queries_today",
"sensor.pi_hole_ads_blocked_today",
"sensor.pi_hole_dns_unique_clients",
],
show_header_toggle: false,
type: "entities",
id: 123123123123213123,
title: "Pi-Hole",
},
{
entities: [
"sensor.plex",
"binary_sensor.gaming_pc",
"binary_sensor.server_1",
"binary_sensor.server_2",
"binary_sensor.windows_server",
"binary_sensor.teamspeak",
"binary_sensor.harmony_hub",
{
style: {
height: "1px",
width: "85%",
"margin-left": "auto",
background: "#62717b",
"margin-right": "auto",
},
type: "divider",
},
// {
// items: ["sensor.uptime_router", "sensor.installerad_routeros"],
// head: {
// entity: "binary_sensor.router",
// },
// type: "custom:fold-entity-row",
// group_config: {
// icon: "mdi:router",
// },
// },
// {
// items: [
// "sensor.uptime_router_server",
// "sensor.installerad_routeros_server",
// ],
// head: {
// entity: "binary_sensor.router_server",
// },
// type: "custom:fold-entity-row",
// group_config: {
// icon: "mdi:router",
// },
// },
],
show_header_toggle: false,
type: "entities",
id: "3e18f63e2c6640d185bf0486a9c4c03f",
title: "N\u00e4tverk",
},
{
entities: [
"binary_sensor.ubiquiti_controller",
"binary_sensor.ubiquiti_switch",
"binary_sensor.ubiquiti_nvr",
"binary_sensor.entre_kamera",
// {
// items: ["sensor.uptime_ap_1"],
// head: {
// entity: "binary_sensor.accesspunkt_1",
// },
// type: "custom:fold-entity-row",
// group_config: {
// icon: "router-wireless",
// },
// },
// {
// items: ["sensor.uptime_ap_2"],
// head: {
// entity: "binary_sensor.accesspunkt_2",
// },
// type: "custom:fold-entity-row",
// group_config: {
// icon: "router-wireless",
// },
// },
"sensor.total_clients_wireless",
],
show_header_toggle: false,
type: "entities",
id: "b8e18e8750224f58b404d0f2e644529a",
title: "Ubiquiti",
},
{
entities: [
"sensor.qbittorrent_up_speed",
"sensor.qbittorrent_down_speed",
"sensor.qbittorrent_status",
],
show_header_toggle: false,
type: "entities",
id: "af8fb9251ce7453ca90c710722b4625b",
title: "Bittorrent",
},
{
entities: [
"sensor.speedtest_download",
"sensor.speedtest_upload",
"sensor.speedtest_ping",
],
show_header_toggle: false,
type: "entities",
id: 12312412,
title: "Bandbredd",
},
// {
// title: "Updater",
// type: "custom:tracker-card",
// trackers: [
// "sensor.custom_card_tracker",
// "sensor.custom_component_tracker",
// ],
// },
],
title: "System & N\u00e4tverk",
id: "system_natverk",
icon: "mdi:server-network",
},
],
});

11
demo/src/configs/types.ts Normal file
View File

@ -0,0 +1,11 @@
import { LovelaceConfig } from "../../../src/data/lovelace";
import { Entity } from "../../../src/fake_data/entity";
export interface DemoConfig {
index?: number;
name: string;
authorName: string;
authorUrl: string;
lovelace: () => LovelaceConfig;
entities: () => Entity[];
}

View File

@ -0,0 +1,86 @@
import { LitElement } from "lit-element";
import "./card-tools";
class CardModder extends LitElement {
setConfig(config) {
if (!window.cardTools)
throw new Error(
`Can't find card-tools. See https://github.com/thomasloven/lovelace-card-tools`
);
window.cardTools.checkVersion(0.2);
if (!config || !config.card) {
throw new Error("Card config incorrect");
}
if (Array.isArray(config.card)) {
throw new Error("It says 'card', not 'cardS'. Remove the dash.");
}
this._config = config;
this.card = window.cardTools.createCard(config.card);
this.templated = [];
this.attempts = 0;
}
render() {
return window.cardTools.litHtml`
<div id="root">${this.card}</div>
`;
}
firstUpdated() {
this._cardMod();
}
_cardMod() {
if (!this._config.style) return;
let target = null;
target = target || this.card.querySelector("ha-card");
target =
target ||
(this.card.shadowRoot && this.card.shadowRoot.querySelector("ha-card"));
target =
target ||
(this.card.firstChild &&
this.card.firstChild.shadowRoot &&
this.card.firstChild.shadowRoot.querySelector("ha-card"));
if (!target && !this.attempts)
// Try twice
setTimeout(() => this._cardMod(), 100);
this.attempts++;
target = target || this.card;
for (var k in this._config.style) {
if (window.cardTools.hasTemplate(this._config.style[k]))
this.templated.push(k);
target.style.setProperty(
k,
window.cardTools.parseTemplate(this._config.style[k])
);
}
this.target = target;
}
set hass(hass) {
if (this.card) this.card.hass = hass;
if (this.templated)
this.templated.forEach((k) => {
this.target.style.setProperty(
k,
window.cardTools.parseTemplate(this._config.style[k], "")
);
});
}
getCardSize() {
if (this._config && this._config.report_size)
return this._config.report_size;
if (this.card)
return typeof this.card.getCardSize === "function"
? this.card.getCardSize()
: 1;
return 1;
}
}
customElements.define("card-modder", CardModder);

View File

@ -0,0 +1,197 @@
import { LitElement, html } from "lit-element";
if (!window.cardTools) {
const version = 0.2;
const CUSTOM_TYPE_PREFIX = "custom:";
let cardTools = {};
cardTools.v = version;
cardTools.checkVersion = (v) => {
if (version < v) {
throw new Error(
`Old version of card-tools found. Get the latest version of card-tools.js from https://github.com/thomasloven/lovelace-card-tools`
);
}
};
cardTools.LitElement = LitElement;
cardTools.litHtml = html;
cardTools.hass = () => {
return document.querySelector("home-assistant").hass;
};
cardTools.fireEvent = (ev, detail) => {
ev = new Event(ev, {
bubbles: true,
cancelable: false,
composed: true,
});
ev.detail = detail || {};
document.querySelector("ha-demo").dispatchEvent(ev);
};
cardTools.createThing = (thing, config) => {
const _createThing = (tag, config) => {
const element = document.createElement(tag);
try {
element.setConfig(config);
} catch (err) {
console.error(tag, err);
return _createError(err.message, config);
}
return element;
};
const _createError = (error, config) => {
return _createThing("hui-error-card", {
type: "error",
error,
config,
});
};
if (!config || typeof config !== "object" || !config.type)
return _createError(`No ${thing} type configured`, config);
let tag = config.type;
if (config.error) {
const err = config.error;
delete config.error;
return _createError(err, config);
}
if (tag.startsWith(CUSTOM_TYPE_PREFIX))
tag = tag.substr(CUSTOM_TYPE_PREFIX.length);
else tag = `hui-${tag}-${thing}`;
if (customElements.get(tag)) return _createThing(tag, config);
// If element doesn't exist (yet) create an error
const element = _createError(
`Custom element doesn't exist: ${tag}.`,
config
);
element.style.display = "None";
const time = setTimeout(() => {
element.style.display = "";
}, 2000);
// Remove error if element is defined later
customElements.whenDefined(tag).then(() => {
clearTimeout(timer);
cardTools.fireEvent("rebuild-view");
});
return element;
};
cardTools.createCard = (config) => {
return cardTools.createThing("card", config);
};
cardTools.createElement = (config) => {
return cardTools.createThing("element", config);
};
cardTools.createEntityRow = (config) => {
const SPECIAL_TYPES = new Set([
"call-service",
"divider",
"section",
"weblink",
]);
const DEFAULT_ROWS = {
alert: "toggle",
automation: "toggle",
climate: "toggle",
cover: "cover",
fan: "toggle",
group: "group",
input_boolean: "toggle",
input_number: "input-number",
input_select: "input-select",
input_text: "input-text",
light: "toggle",
media_player: "media-player",
lock: "lock",
scene: "scene",
script: "script",
sensor: "sensor",
timer: "timer",
switch: "toggle",
vacuum: "toggle",
};
if (
!config ||
typeof config !== "object" ||
(!config.entity && !config.type)
) {
Object.assign(config, { error: "Invalid config given" });
return cardTools.createThing("", config);
}
const type = config.type || "default";
if (SPECIAL_TYPES.has(type) || type.startsWith(CUSTOM_TYPE_PREFIX))
return cardTools.createThing("row", config);
const domain = config.entity.split(".", 1)[0];
Object.assign(config, { type: DEFAULT_ROWS[domain] || "text" });
return cardTools.createThing("entity-row", config);
};
cardTools.deviceID = (() => {
const ID_STORAGE_KEY = "lovelace-player-device-id";
if (window["fully"] && typeof fully.getDeviceId === "function")
return fully.getDeviceId();
if (!localStorage[ID_STORAGE_KEY]) {
const s4 = () => {
return Math.floor((1 + Math.random()) * 100000)
.toString(16)
.substring(1);
};
localStorage[ID_STORAGE_KEY] = `${s4()}${s4()}-${s4()}${s4()}`;
}
return localStorage[ID_STORAGE_KEY];
})();
cardTools.moreInfo = (entity) => {
cardTools.fireEvent("hass-more-info", { entityId: entity });
};
cardTools.longpress = (element) => {
customElements.whenDefined("long-press").then(() => {
const longpress = document.body.querySelector("long-press");
longpress.bind(element);
});
return element;
};
cardTools.hasTemplate = (text) => {
return /\[\[\s+.*\s+\]\]/.test(text);
};
cardTools.parseTemplate = (text, error) => {
const _parse = (str) => {
try {
str = str.replace(/^\[\[\s+|\s+\]\]$/g, "");
const parts = str.split(".");
let v = cardTools.hass().states[`${parts[0]}.${parts[1]}`];
parts.shift();
parts.shift();
parts.forEach((item) => (v = v[item]));
return v;
} catch (err) {
return error || `[[ Template matching failed ${str} ]]`;
}
};
text = text.replace(/(\[\[\s.*?\s\]\])/g, (str, p1, offset, s) =>
_parse(str)
);
return text;
};
window.cardTools = cardTools;
cardTools.fireEvent("rebuild-view");
}

58
demo/src/entities.ts Normal file
View File

@ -0,0 +1,58 @@
import { getEntity } from "../../src/fake_data/entity";
export const entities = [
getEntity("light", "bed_light", "on", {
friendly_name: "Bed Light",
}),
getEntity("group", "kitchen", "on", {
entity_id: ["light.bed_light"],
order: 8,
friendly_name: "Kitchen",
}),
getEntity("lock", "kitchen_door", "locked", {
friendly_name: "Kitchen Door",
}),
getEntity("cover", "kitchen_window", "open", {
friendly_name: "Kitchen Window",
supported_features: 11,
}),
getEntity("scene", "romantic_lights", "scening", {
entity_id: ["light.bed_light", "light.ceiling_lights"],
friendly_name: "Romantic lights",
}),
getEntity("device_tracker", "demo_paulus", "home", {
source_type: "gps",
latitude: 32.877105,
longitude: 117.232185,
gps_accuracy: 91,
battery: 71,
friendly_name: "Paulus",
}),
getEntity("climate", "ecobee", "auto", {
current_temperature: 73,
min_temp: 45,
max_temp: 95,
temperature: null,
target_temp_high: 75,
target_temp_low: 70,
fan_mode: "Auto Low",
fan_list: ["On Low", "On High", "Auto Low", "Auto High", "Off"],
operation_mode: "auto",
operation_list: ["heat", "cool", "auto", "off"],
hold_mode: "home",
swing_mode: "Auto",
swing_list: ["Auto", "1", "2", "3", "Off"],
unit_of_measurement: "°F",
friendly_name: "Ecobee",
supported_features: 1014,
}),
getEntity("input_number", "noise_allowance", 5, {
min: 0,
max: 10,
step: 1,
mode: "slider",
unit_of_measurement: "dB",
friendly_name: "Allowed Noise",
icon: "mdi:bell-ring",
}),
];

19
demo/src/entrypoint.ts Normal file
View File

@ -0,0 +1,19 @@
import "@polymer/paper-styles/typography";
import "@polymer/polymer/lib/elements/dom-if";
import "@polymer/polymer/lib/elements/dom-repeat";
import "../../src/resources/hass-icons";
import "../../src/resources/ha-style";
import "../../src/resources/roboto";
import "../../src/components/ha-iconset-svg";
import "./ha-demo";
/* polyfill for paper-dropdown */
setTimeout(
() =>
import(/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"),
1000
);
document.body.appendChild(document.createElement("ha-demo"));

139
demo/src/ha-demo-card.ts Normal file
View File

@ -0,0 +1,139 @@
import { LitElement, html, CSSResult, css } from "lit-element";
import { until } from "lit-html/directives/until";
import "@polymer/paper-icon-button";
import "../../src/components/ha-card";
import { LovelaceCard, Lovelace } from "../../src/panels/lovelace/types";
import { LovelaceCardConfig } from "../../src/data/lovelace";
import { MockHomeAssistant } from "../../src/fake_data/provide_hass";
import {
demoConfigs,
selectedDemoConfig,
setDemoConfig,
selectedDemoConfigIndex,
} from "./configs/demo-configs";
export class HADemoCard extends LitElement implements LovelaceCard {
public lovelace?: Lovelace;
public hass?: MockHomeAssistant;
public getCardSize() {
return 2;
}
public setConfig(
// @ts-ignore
config: LovelaceCardConfig
// tslint:disable-next-line:no-empty
) {}
protected render() {
return html`
<ha-card header="Home Assistant Demo Switcher">
<div class="picker">
<paper-icon-button
@click=${this._prevConfig}
icon="hass:chevron-right"
style="transform: rotate(180deg)"
></paper-icon-button>
<div>
${
until(
selectedDemoConfig.then(
(conf) => html`
${conf.name}
<small>
by
<a target="_blank" href="${conf.authorUrl}">
${conf.authorName}
</a>
</small>
`
),
""
)
}
</div>
<paper-icon-button
@click=${this._nextConfig}
icon="hass:chevron-right"
></paper-icon-button>
</div>
</ha-card>
`;
}
private _prevConfig() {
this._updateConfig(
selectedDemoConfigIndex > 0
? selectedDemoConfigIndex - 1
: demoConfigs.length - 1
);
}
private _nextConfig() {
this._updateConfig(
selectedDemoConfigIndex < demoConfigs.length - 1
? selectedDemoConfigIndex + 1
: 0
);
}
private _updateConfig(index: number) {
setDemoConfig(this.hass!, this.lovelace!, index);
}
static get styles(): CSSResult[] {
return [
css`
.content {
padding: 0 16px;
}
ul {
margin-top: 0;
margin-bottom: 0;
padding: 16px 16px 16px 38px;
}
li {
padding: 8px 0;
}
li:first-child {
margin-top: -8px;
}
li:last-child {
margin-bottom: -8px;
}
a {
color: var(--primary-color);
}
.picker {
display: flex;
justify-content: space-between;
align-items: center;
height: 60px;
}
.picker div {
text-align: center;
}
.picker small {
display: block;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-demo-card": HADemoCard;
}
}
customElements.define("ha-demo-card", HADemoCard);

72
demo/src/ha-demo.ts Normal file
View File

@ -0,0 +1,72 @@
import { HomeAssistant } from "../../src/layouts/app/home-assistant";
import { provideHass } from "../../src/fake_data/provide_hass";
import { navigate } from "../../src/common/navigate";
import { mockLovelace } from "./lovelace";
import { mockAuth } from "./auth";
import { selectedDemoConfig } from "./configs/demo-configs";
class HaDemo extends HomeAssistant {
protected async _handleConnProm() {
const hass = provideHass(this, {
panelUrl: (this as any).panelUrl,
});
mockLovelace(hass);
mockAuth(hass);
selectedDemoConfig.then((conf) => hass.addEntities(conf.entities()));
// Taken from polymer/pwa-helpers. BSD-3 licensed
document.body.addEventListener(
"click",
(e) => {
if (
e.defaultPrevented ||
e.button !== 0 ||
e.metaKey ||
e.ctrlKey ||
e.shiftKey
) {
return;
}
const anchor = e
.composedPath()
.filter((n) => (n as HTMLElement).tagName === "A")[0] as
| HTMLAnchorElement
| undefined;
if (
!anchor ||
anchor.target ||
anchor.hasAttribute("download") ||
anchor.getAttribute("rel") === "external"
) {
return;
}
let href = anchor.href;
if (!href || href.indexOf("mailto:") !== -1) {
return;
}
const location = window.location;
const origin =
location.origin || location.protocol + "//" + location.host;
if (href.indexOf(origin) !== 0) {
return;
}
href = href.substr(origin.length);
if (href === "#") {
return;
}
e.preventDefault();
navigate(this as any, href);
},
{ capture: true }
);
(this as any).hassConnected();
}
}
customElements.define("ha-demo", HaDemo);

31
demo/src/lovelace.ts Normal file
View File

@ -0,0 +1,31 @@
import { entities } from "./entities";
import "./ha-demo-card";
// Not duplicate, one is for typing.
// tslint:disable-next-line
import { HADemoCard } from "./ha-demo-card";
import { MockHomeAssistant } from "../../src/fake_data/provide_hass";
import { HUIView } from "../../src/panels/lovelace/hui-view";
import { selectedDemoConfig } from "./configs/demo-configs";
export const mockLovelace = (hass: MockHomeAssistant) => {
hass.addEntities(entities);
hass.mockWS("lovelace/config", () =>
selectedDemoConfig.then((config) => config.lovelace())
);
hass.mockWS("frontend/get_translations", () => Promise.resolve({}));
hass.mockWS("lovelace/config/save", () => Promise.resolve());
};
// Patch HUI-VIEW to make the lovelace object available to the demo card
const oldCreateCard = HUIView.prototype.createCardElement;
HUIView.prototype.createCardElement = function(config) {
const el = oldCreateCard.call(this, config);
if (el.tagName === "HA-DEMO-CARD") {
(el as HADemoCard).lovelace = this.lovelace;
}
return el;
};

100
demo/webpack.config.js Normal file
View File

@ -0,0 +1,100 @@
const path = require("path");
const webpack = require("webpack");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { babelLoaderConfig } = require("../config/babel.js");
const isProd = process.env.NODE_ENV === "production";
const chunkFilename = isProd ? "chunk.[chunkhash].js" : "[name].chunk.js";
const buildPath = path.resolve(__dirname, "dist");
const publicPath = isProd ? "./" : "http://localhost:8080/";
const latestBuild = false;
module.exports = {
mode: isProd ? "production" : "development",
// Disabled in prod while we make Home Assistant able to serve the right files.
// Was source-map
devtool: isProd ? "none" : "inline-source-map",
entry: {
main: "./src/entrypoint.ts",
compatibility: "../src/entrypoints/compatibility.js",
},
module: {
rules: [
babelLoaderConfig({ latestBuild }),
{
test: /\.css$/,
use: "raw-loader",
},
{
test: /\.(html)$/,
use: {
loader: "html-loader",
options: {
exportAsEs6Default: true,
},
},
},
],
},
plugins: [
new webpack.DefinePlugin({
__DEV__: false,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify("DEMO"),
__DEMO__: true,
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
isProd ? "production" : "development"
),
}),
new CopyWebpackPlugin([
"public",
"../node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js",
{ from: "../public", to: "static" },
{ from: "../build-translations/output", to: "static/translations" },
{
from: "../node_modules/leaflet/dist/leaflet.css",
to: "static/images/leaflet/",
},
{
from: "../node_modules/@polymer/font-roboto-local/fonts",
to: "static/fonts",
},
{
from: "../node_modules/leaflet/dist/images",
to: "static/images/leaflet/",
},
]),
isProd &&
new UglifyJsPlugin({
extractComments: true,
sourceMap: true,
uglifyOptions: {
// Disabling because it broke output
mangle: false,
},
}),
].filter(Boolean),
resolve: {
extensions: [".ts", ".js", ".json"],
alias: {
react: "preact-compat",
"react-dom": "preact-compat",
// Not necessary unless you consume a module using `createClass`
"create-react-class": "preact-compat/lib/create-react-class",
// Not necessary unless you consume a module requiring `react-dom-factories`
"react-dom-factories": "preact-compat/lib/react-dom-factories",
},
},
output: {
filename: "[name].js",
chunkFilename: chunkFilename,
path: buildPath,
publicPath,
},
devServer: {
contentBase: "./public",
},
};

View File

@ -1,13 +0,0 @@
import config from './config_data';
import events from './event_data';
import services from './service_data';
import states from './state_data';
import panels from './panel_data';
export default {
config,
events,
panels,
services,
states,
};

View File

@ -1,18 +0,0 @@
export default {
components: [
'configurator',
'http',
'api',
'frontend',
'history',
'conversation',
'logbook',
'introduction',
],
latitude: 32.87336,
location_name: 'Home',
longitude: -117.22743,
temperature_unit: '\u00b0F',
time_zone: 'America/Los_Angeles',
version: '0.26',
};

View File

@ -1,5 +0,0 @@
import bootstrap from './bootstrap_data';
import logbook from './logbook_data';
import stateHistory from './state_history_data';
window.hassDemoData = { bootstrap, logbook, stateHistory };

View File

@ -1,18 +0,0 @@
export default [
{
event: 'call_service',
listener_count: 1,
},
{
event: 'time_changed',
listener_count: 1,
},
{
event: 'state_changed',
listener_count: 3,
},
{
event: 'homeassistant_stop',
listener_count: 2,
},
];

View File

@ -1,93 +0,0 @@
export default [
{
domain: 'sun',
entity_id: 'sun.sun',
message: 'has risen',
name: 'sun',
when: '2015-04-24T06:08:47.000Z',
},
{
domain: 'device_tracker',
entity_id: 'device_tracker.paulus',
message: 'left home',
name: 'Paulus',
when: '2015-04-24T08:54:47.000Z',
},
{
domain: 'device_tracker',
entity_id: 'device_tracker.anne_therese',
message: 'left home',
name: 'Anne Therese',
when: '2015-04-24T09:08:47.000Z',
},
{
domain: 'group',
entity_id: 'group.all_devices',
message: 'left home',
name: 'All devices',
when: '2015-04-24T09:08:47.000Z',
},
{
domain: 'thermostat',
entity_id: 'thermostat.nest',
message: 'changed to 17 \u00b0C',
name: 'Nest',
when: '2015-04-24T09:08:47.000Z',
},
{
domain: 'thermostat',
entity_id: 'thermostat.nest',
message: 'changed to 21 \u00b0C',
name: 'Nest',
when: '2015-04-24T16:00:47.000Z',
},
{
domain: 'device_tracker',
entity_id: 'device_tracker.anne_therese',
message: 'came home',
name: 'Anne Therese',
when: '2015-04-24T16:24:47.000Z',
},
{
domain: 'group',
entity_id: 'group.all_devices',
message: 'came home',
name: 'All devices',
when: '2015-04-24T16:24:47.000Z',
},
{
domain: 'light',
entity_id: 'light.bowl',
message: 'turned on',
name: 'Bowl',
when: '2015-04-24T18:01:47.000Z',
},
{
domain: 'light',
entity_id: 'light.ceiling',
message: 'turned on',
name: 'Ceiling',
when: '2015-04-24T18:16:47.000Z',
},
{
domain: 'light',
entity_id: 'light.tv_back_light',
message: 'turned on',
name: 'TV Back Light',
when: '2015-04-24T18:31:47.000Z',
},
{
domain: 'sun',
entity_id: 'sun.sun',
message: 'has set',
name: 'sun',
when: '2015-04-24T18:46:47.000Z',
},
{
domain: 'media_player',
entity_id: 'media_player.living_room',
message: 'changed to Plex',
name: 'Media Player',
when: '2015-04-24T19:12:47.000Z',
},
];

View File

@ -1,48 +0,0 @@
export default {
'dev-event': {
component_name: 'dev-event',
url: '/demo/panels/ha-panel-dev-event.html',
url_name: 'dev-event',
},
'dev-info': {
component_name: 'dev-info',
url: '/demo/panels/ha-panel-dev-info.html',
url_name: 'dev-info',
},
'dev-service': {
component_name: 'dev-service',
url: '/demo/panels/ha-panel-dev-service.html',
url_name: 'dev-service',
},
'dev-state': {
component_name: 'dev-state',
url: '/demo/panels/ha-panel-dev-state.html',
url_name: 'dev-state',
},
'dev-template': {
component_name: 'dev-template',
url: '/demo/panels/ha-panel-dev-template.html',
url_name: 'dev-template',
},
history: {
component_name: 'history',
icon: 'mdi:poll-box',
title: 'History',
url: '/demo/panels/ha-panel-history.html',
url_name: 'history',
},
logbook: {
component_name: 'logbook',
icon: 'mdi:format-list-bulleted-type',
title: 'Logbook',
url: '/demo/panels/ha-panel-logbook.html',
url_name: 'logbook',
},
map: {
component_name: 'map',
icon: 'mdi:account-location',
title: 'Map',
url: '/demo/panels/ha-panel-map.html',
url_name: 'map',
},
};

View File

@ -1,37 +0,0 @@
export default [
{
domain: 'homeassistant',
services: {
stop: { description: '', fields: {} },
turn_off: { description: '', fields: {} },
turn_on: { description: '', fields: {} },
},
},
{
domain: 'light',
services: {
turn_off: { description: '', fields: {} },
turn_on: { description: '', fields: {} },
},
},
{
domain: 'switch',
services: {
turn_off: { description: '', fields: {} },
turn_on: { description: '', fields: {} },
},
},
{
domain: 'input_boolean',
services: {
turn_off: { description: '', fields: {} },
turn_on: { description: '', fields: {} },
},
},
{
domain: 'configurator',
services: {
configure: { description: '', fields: {} },
},
},
];

View File

@ -1,279 +0,0 @@
function getRandomTime() {
const ts = new Date(new Date().getTime() - (Math.random() * 80 * 60 * 1000));
return ts.toISOString();
}
const entities = [];
function addEntity(entityId, state, attributes = {}) {
entities.push({
state,
attributes,
entity_id: entityId,
last_changed: getRandomTime(),
last_updated: getRandomTime(),
});
}
let groupOrder = 0;
function addGroup(objectId, state, entityIds, name, view) {
groupOrder++;
const attributes = {
entity_id: entityIds,
order: groupOrder,
};
if (name) {
attributes.friendly_name = name;
}
if (view) {
attributes.view = view;
attributes.hidden = true;
}
addEntity(`group.${objectId}`, state, attributes);
}
// ---------------------------------------------------
// HOME ASSISTANT
// ---------------------------------------------------
addEntity('a.demo_mode', 'enabled');
addEntity('configurator.philips_hue', 'configure', {
configure_id: '4415244496-1',
description: 'Press the button on the bridge to register Philips Hue with Home Assistant.',
description_image: '/demo/images/config_philips_hue.jpg',
fields: [],
submit_caption: 'I have pressed the button',
friendly_name: 'Philips Hue',
});
// ---------------------------------------------------
// VIEWS
// ---------------------------------------------------
addGroup(
'default_view', 'on', [
'a.demo_mode',
'sensor.humidity',
'sensor.temperature',
'device_tracker.paulus',
'device_tracker.anne_therese',
'configurator.philips_hue',
'group.cooking',
'group.general',
'group.rooms',
'camera.living_room',
'media_player.living_room',
'scene.romantic',
'scene.good_morning',
'script.water_lawn',
], 'Main', true);
addGroup(
'rooms_view', 'on', [
'group.living_room',
'group.bedroom',
], 'Rooms', true);
addGroup('rooms', 'on', ['group.living_room', 'group.bedroom'], 'Rooms');
// ---------------------------------------------------
// DEVICE TRACKER + ZONES
// ---------------------------------------------------
addEntity('device_tracker.anne_therese', 'school', {
entity_picture: 'https://graph.facebook.com/621994601/picture',
friendly_name: 'Anne Therese',
latitude: 32.879898,
longitude: -117.236776,
gps_accuracy: 250,
battery: 76,
});
addEntity('device_tracker.paulus', 'not_home', {
entity_picture: 'https://graph.facebook.com/297400035/picture',
friendly_name: 'Paulus',
gps_accuracy: 75,
latitude: 32.892950,
longitude: -117.203431,
battery: 56,
});
addEntity('zone.school', 'zoning', {
radius: 250,
latitude: 32.880834,
longitude: -117.237556,
icon: 'mdi:library',
hidden: true,
});
addEntity('zone.work', 'zoning', {
radius: 250,
latitude: 32.896844,
longitude: -117.202204,
icon: 'mdi:worker',
hidden: true,
});
addEntity('zone.home', 'zoning', {
radius: 100,
latitude: 32.873708,
longitude: -117.226590,
icon: 'mdi:home',
hidden: true,
});
// ---------------------------------------------------
// GENERAL
// ---------------------------------------------------
addGroup('general', 'on', [
'alarm_control_panel.home',
'garage_door.garage_door',
'lock.kitchen_door',
'thermostat.nest',
'camera.living_room',
]);
addEntity('camera.living_room', 'idle', {
entity_picture: '/demo/webcam.jpg?',
});
addEntity('garage_door.garage_door', 'open', {
friendly_name: 'Garage Door',
});
addEntity('alarm_control_panel.home', 'armed_home', {
friendly_name: 'Alarm',
code_format: '^\\d{4}',
});
addEntity('lock.kitchen_door', 'open', {
friendly_name: 'Kitchen Door',
});
// ---------------------------------------------------
// PRESETS
// ---------------------------------------------------
addEntity('script.water_lawn', 'off', {
friendly_name: 'Water Lawn',
});
addEntity('scene.romantic', 'scening', {
friendly_name: 'Romantic',
});
// addEntity('scene.good_morning', 'scening', {
// friendly_name: 'Good Morning',
// });
// ---------------------------------------------------
// LIVING ROOM
// ---------------------------------------------------
addGroup(
'living_room', 'on',
[
'light.table_lamp',
'light.ceiling',
'light.tv_back_light',
'switch.ac',
'media_player.living_room',
],
'Living Room'
);
addEntity('light.tv_back_light', 'off', {
friendly_name: 'TV Back Light',
});
addEntity('light.ceiling', 'on', {
friendly_name: 'Ceiling Lights',
brightness: 200,
rgb_color: [255, 116, 155],
});
addEntity('light.table_lamp', 'on', {
brightness: 200,
rgb_color: [150, 212, 94],
friendly_name: 'Table Lamp',
});
addEntity('switch.ac', 'on', {
friendly_name: 'AC',
icon: 'mdi:air-conditioner',
});
addEntity('media_player.living_room', 'playing', {
entity_picture: '/demo/images/thrones.jpg',
friendly_name: 'Chromecast',
supported_features: 509,
media_content_type: 'tvshow',
media_title: 'The Dance of Dragons',
media_series_title: 'Game of Thrones',
media_season: 5,
media_episode: '09',
app_name: 'HBO Now',
});
// ---------------------------------------------------
// BEDROOM
// ---------------------------------------------------
addGroup(
'bedroom', 'off',
[
'light.bed_light',
'switch.decorative_lights',
'rollershutter.bedroom_window',
],
'Bedroom'
);
addEntity('switch.decorative_lights', 'off', {
friendly_name: 'Decorative Lights',
});
addEntity('light.bed_light', 'off', {
friendly_name: 'Bed Light',
});
addEntity('rollershutter.bedroom_window', 'closed', {
friendly_name: 'Window',
current_position: 0,
});
// ---------------------------------------------------
// SENSORS
// ---------------------------------------------------
addEntity('sensor.temperature', '15.6', {
unit_of_measurement: '\u00b0C',
friendly_name: 'Temperature',
});
addEntity('sensor.humidity', '54', {
unit_of_measurement: '%',
friendly_name: 'Humidity',
});
addEntity('thermostat.nest', '23', {
away_mode: 'off',
temperature: '21',
current_temperature: '18',
unit_of_measurement: '\u00b0C',
friendly_name: 'Nest',
});
// ---------------------------------------------------
// COOKING AUTOMATION
// ---------------------------------------------------
addEntity('input_select.cook_today', 'Paulus', {
options: ['Paulus', 'Anne Therese'],
icon: 'mdi:panda',
});
addEntity('input_boolean.notify_cook', 'on', {
icon: 'mdi:alarm',
friendly_name: 'Notify Cook',
});
addGroup(
'cooking', 'unknown',
['input_select.cook_today', 'input_boolean.notify_cook']
);
export default entities;

View File

@ -1,255 +0,0 @@
import stateData from './state_data';
function getTime(minutesAgo) {
const ts = new Date(Date.now() - (minutesAgo * 60 * 1000));
return ts.toISOString();
}
// prefill with entities we do not want to track
const seen = {
'a.demo_mode': true,
'configurator.philips_hue': true,
'group.default_view': true,
'group.rooms_view': true,
'group.rooms': true,
'zone.school': true,
'zone.work': true,
'zone.home': true,
'group.general': true,
'camera.roundabout': true,
'script.water_lawn': true,
'scene.romantic': true,
'scene.good_morning': true,
'group.cooking': true,
};
const history = [];
function randomTimeAdjustment(diff) {
return Math.random() * diff - (diff / 2);
}
const maxTime = 1440;
function addEntity(state, deltas) {
seen[state.entity_id] = true;
let changes;
if (typeof deltas[0] === 'string') {
changes = deltas.map(state_ => ({ state: state_ }));
} else {
changes = deltas;
}
const timeDiff = (900 / changes.length);
history.push(changes.map(
(change, index) => {
let attributes;
if (!change.attributes && !state.attributes) {
attributes = {};
} else if (!change.attributes) {
attributes = state.attributes;
} else if (!state.attributes) {
attributes = change.attributes;
} else {
attributes = Object.assign({}, state.attributes, change.attributes);
}
const time = index === 0 ? getTime(maxTime) : getTime(maxTime - index * timeDiff +
randomTimeAdjustment(timeDiff));
return {
attributes,
entity_id: state.entity_id,
state: change.state || state.state,
last_changed: time,
last_updated: time,
};
}));
}
addEntity(
{
entity_id: 'sensor.humidity',
attributes: {
unit_of_measurement: '%',
},
}, ['45', '49', '52', '49', '52', '49', '45', '42']
);
addEntity(
{
entity_id: 'sensor.temperature',
attributes: {
unit_of_measurement: '\u00b0C',
},
}, ['23', '27', '25', '23', '24']
);
addEntity(
{
entity_id: 'thermostat.nest',
attributes: {
unit_of_measurement: '\u00b0C',
},
}, [
{
state: '23',
attributes: {
current_temperature: 20,
temperature: 23,
},
},
{
state: '23',
attributes: {
current_temperature: 22,
temperature: 23,
},
},
{
state: '20',
attributes: {
current_temperature: 21,
temperature: 20,
},
},
{
state: '20',
attributes: {
current_temperature: 20,
temperature: 20,
},
},
{
state: '20',
attributes: {
current_temperature: 19,
temperature: 20,
},
},
]
);
addEntity(
{
entity_id: 'media_player.living_room',
attributes: {
friendly_name: 'Chromecast',
},
}, ['Plex', 'idle', 'YouTube', 'Netflix', 'idle', 'Plex']
);
addEntity(
{
entity_id: 'group.all_devices',
}, ['home', 'not_home', 'home']
);
addEntity(
{
entity_id: 'device_tracker.paulus',
}, ['home', 'not_home', 'work', 'not_home']
);
addEntity(
{
entity_id: 'device_tracker.anne_therese',
}, ['home', 'not_home', 'home', 'not_home', 'school']
);
addEntity(
{
entity_id: 'garage_door.garage_door',
}, ['open', 'closed', 'open']
);
addEntity(
{
entity_id: 'alarm_control_panel.home',
}, ['disarmed', 'pending', 'armed_home', 'pending', 'disarmed', 'pending', 'armed_home']
);
addEntity(
{
entity_id: 'lock.kitchen_door',
}, ['unlocked', 'locked', 'unlocked', 'locked']
);
addEntity(
{
entity_id: 'light.tv_back_light',
}, ['on', 'off', 'on', 'off']
);
addEntity(
{
entity_id: 'light.ceiling',
}, ['on', 'off', 'on']
);
addEntity(
{
entity_id: 'light.table_lamp',
}, ['on', 'off', 'on']
);
addEntity(
{
entity_id: 'switch.ac',
}, ['on', 'off', 'on']
);
addEntity(
{
entity_id: 'group.bedroom',
}, ['on', 'off', 'on', 'off']
);
addEntity(
{
entity_id: 'group.living_room',
}, ['on', 'off', 'on']
);
addEntity(
{
entity_id: 'switch.decorative_lights',
}, ['on', 'off', 'on', 'off']
);
addEntity(
{
entity_id: 'light.bed_light',
}, ['on', 'off', 'on', 'off']
);
addEntity(
{
entity_id: 'rollershutter.bedroom_window',
}, ['open', 'closed', 'open', 'closed']
);
addEntity(
{
entity_id: 'input_select.cook_today',
}, ['Anne Therese', 'Paulus']
);
addEntity(
{
entity_id: 'input_boolean.notify_cook',
}, ['off', 'on']
);
if (__DEV__) {
for (let i = 0; i < stateData.length; i++) {
const entity = stateData[i];
if (!(entity.entity_id in seen)) {
/* eslint-disable no-console */
console.warn(`Missing history for ${entity.entity_id}`);
/* eslint-enable no-console */
}
}
}
export default history;

View File

@ -3,9 +3,9 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import JsYaml from "js-yaml";
import HomeAssistant from "../data/hass";
import { demoConfig } from "../data/demo_config";
import { demoServices } from "../data/demo_services";
import demoResources from "../data/demo_resources";
import { demoConfig } from "../../../src/fake_data/demo_config";
import { demoServices } from "../../../src/fake_data/demo_services";
import demoResources from "../../../src/fake_data/demo_resources";
import demoStates from "../data/demo_states";
import { createCardElement } from "../../../src/panels/lovelace/common/create-card-element";

View File

@ -1,11 +0,0 @@
export const demoConfig = {
elevation: 300,
latitude: 51.5287352,
longitude: -0.381773,
unit_system: {
length: "km",
mass: "kg",
temperature: "°C",
volume: "L",
},
};

View File

@ -1,96 +0,0 @@
export const demoServices = {
configurator: ["configure"],
tts: ["demo_say", "clear_cache"],
cover: [
"open_cover",
"close_cover",
"open_cover_tilt",
"close_cover_tilt",
"set_cover_tilt_position",
"set_cover_position",
"stop_cover_tilt",
"stop_cover",
],
group: ["set", "reload", "remove", "set_visibility"],
alarm_control_panel: [
"alarm_arm_night",
"alarm_disarm",
"alarm_trigger",
"alarm_arm_home",
"alarm_arm_away",
"alarm_arm_custom_bypass",
],
conversation: ["process"],
notify: ["demo_test_target_name", "notify"],
lock: ["open", "lock", "unlock"],
input_select: [
"select_previous",
"set_options",
"select_next",
"select_option",
],
recorder: ["purge"],
persistent_notification: ["create", "dismiss"],
timer: ["pause", "cancel", "finish", "start"],
input_boolean: ["turn_off", "toggle", "turn_on"],
fan: [
"set_speed",
"turn_on",
"turn_off",
"set_direction",
"oscillate",
"toggle",
],
climate: [
"set_humidity",
"set_operation_mode",
"set_aux_heat",
"turn_on",
"set_hold_mode",
"set_away_mode",
"turn_off",
"set_fan_mode",
"set_temperature",
"set_swing_mode",
],
switch: ["turn_off", "toggle", "turn_on"],
script: ["turn_off", "demo", "reload", "toggle", "turn_on"],
scene: ["turn_on"],
system_log: ["clear", "write"],
camera: ["disable_motion_detection", "enable_motion_detection", "snapshot"],
image_processing: ["scan"],
media_player: [
"media_previous_track",
"clear_playlist",
"shuffle_set",
"media_seek",
"turn_on",
"media_play_pause",
"media_next_track",
"media_pause",
"volume_down",
"volume_set",
"media_stop",
"toggle",
"media_play",
"play_media",
"volume_mute",
"turn_off",
"select_sound_mode",
"select_source",
"volume_up",
],
input_number: ["set_value", "increment", "decrement"],
device_tracker: ["see"],
homeassistant: [
"stop",
"check_config",
"reload_core_config",
"turn_on",
"turn_off",
"restart",
"toggle",
],
light: ["turn_off", "toggle", "turn_on"],
input_text: ["set_value"],
};

View File

@ -1,112 +0,0 @@
import { fireEvent } from "../../../src/common/dom/fire_event";
import { demoConfig } from "./demo_config";
import { demoServices } from "./demo_services";
import demoResources from "./demo_resources";
const ensureArray = (val) => (Array.isArray(val) ? val : [val]);
export default (elements, { initialStates = {} } = {}) => {
elements = ensureArray(elements);
const wsCommands = {};
const restResponses = {};
let hass;
const entities = {};
function updateHass(obj) {
hass = Object.assign({}, hass, obj);
elements.forEach((el) => {
el.hass = hass;
});
}
updateHass({
// Home Assistant properties
config: demoConfig,
services: demoServices,
language: "en",
resources: demoResources,
states: initialStates,
themes: {},
connection: {
subscribeEvents: async (callback, event) => {
console.log("subscribeEvents", event);
return () => console.log("unsubscribeEvents", event);
},
},
// Mock properties
mockEntities: entities,
// Home Assistant functions
async callService(domain, service, data) {
fireEvent(elements[0], "show-notification", {
message: `Called service ${domain}/${service}`,
});
if (data.entity_id) {
await Promise.all(
ensureArray(data.entity_id).map((ent) =>
entities[ent].handleService(domain, service, data)
)
);
} else {
console.log("unmocked callService", domain, service, data);
}
},
async callWS(msg) {
const callback = wsCommands[msg.type];
return callback
? callback(msg)
: Promise.reject({
code: "command_not_mocked",
message: "This command is not implemented in the gallery.",
});
},
async sendWS(msg) {
const callback = wsCommands[msg.type];
if (callback) {
callback(msg);
} else {
console.error(`Unknown command: ${msg.type}`);
}
console.log("sendWS", msg);
},
async callApi(method, path, parameters) {
const callback = restResponses[path];
return callback
? callback(method, path, parameters)
: Promise.reject(`Mock for {path} is not implemented`);
},
// Mock functions
updateHass,
updateStates(newStates) {
updateHass({
states: Object.assign({}, hass.states, newStates),
});
},
addEntities(newEntities) {
const states = {};
ensureArray(newEntities).forEach((ent) => {
ent.hass = hass;
entities[ent.entityId] = ent;
states[ent.entityId] = ent.toState();
});
this.updateStates(states);
},
mockWS(type, callback) {
wsCommands[type] = callback;
},
mockAPI(path, callback) {
restResponses[path] = callback;
},
});
return hass;
};

View File

@ -1,8 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import getEntity from "../data/entity";
import provideHass from "../data/provide_hass";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
const ENTITIES = [

View File

@ -1,8 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import getEntity from "../data/entity";
import provideHass from "../data/provide_hass";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
const ENTITIES = [

View File

@ -1,8 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import getEntity from "../data/entity";
import provideHass from "../data/provide_hass";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
const ENTITIES = [

View File

@ -1,8 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import getEntity from "../data/entity";
import provideHass from "../data/provide_hass";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
const ENTITIES = [

View File

@ -1,8 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import getEntity from "../data/entity";
import provideHass from "../data/provide_hass";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
const ENTITIES = [

View File

@ -1,8 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import getEntity from "../data/entity";
import provideHass from "../data/provide_hass";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
const ENTITIES = [

View File

@ -1,8 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import getEntity from "../data/entity";
import provideHass from "../data/provide_hass";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
const ENTITIES = [

View File

@ -1,8 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import getEntity from "../data/entity";
import provideHass from "../data/provide_hass";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
const ENTITIES = [

View File

@ -1,8 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import getEntity from "../data/entity";
import provideHass from "../data/provide_hass";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
const ENTITIES = [

View File

@ -1,8 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import getEntity from "../data/entity";
import provideHass from "../data/provide_hass";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
const ENTITIES = [

View File

@ -1,7 +1,7 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import provideHass from "../data/provide_hass";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
const CONFIGS = [

View File

@ -1,8 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import getEntity from "../data/entity";
import provideHass from "../data/provide_hass";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
const ENTITIES = [

View File

@ -1,8 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import getEntity from "../data/entity";
import provideHass from "../data/provide_hass";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
const ENTITIES = [

View File

@ -4,8 +4,8 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/dialogs/more-info/controls/more-info-content";
import "../../../src/components/ha-card";
import getEntity from "../data/entity";
import provideHass from "../data/provide_hass";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-more-infos";
import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";

View File

@ -2,12 +2,13 @@ const path = require("path");
const gulp = require("gulp");
const foreach = require("gulp-foreach");
const hash = require("gulp-hash");
const insert = require("gulp-insert");
const merge = require("gulp-merge-json");
const minify = require("gulp-jsonminify");
const rename = require("gulp-rename");
const transform = require("gulp-json-transform");
const isDemo = process.env.DEMO === "1";
const inDir = "translations";
const workDir = "build-translations";
const fullDir = workDir + "/full";
@ -230,7 +231,7 @@ gulp.task(taskName, ["build-flattened-translations"], function() {
hash({
algorithm: "md5",
hashLength: 32,
template: "<%= name %>-<%= hash %>.json",
template: isDemo ? "<%= name %>.json" : "<%= name %>-<%= hash %>.json",
})
)
.pipe(hash.manifest("translationFingerprints.json"))

View File

@ -5,10 +5,18 @@ export const navigate = (
path: string,
replace: boolean = false
) => {
if (replace) {
history.replaceState(null, "", path);
if (__DEMO__) {
if (replace) {
history.replaceState(null, "", `${location.pathname}#${path}`);
} else {
window.location.hash = path;
}
} else {
history.pushState(null, "", path);
if (replace) {
history.replaceState(null, "", path);
} else {
history.pushState(null, "", path);
}
}
fireEvent(node, "location-changed");
};

View File

@ -0,0 +1,19 @@
import { HassConfig } from "home-assistant-js-websocket";
export const demoConfig: HassConfig = {
location_name: "Home",
elevation: 300,
latitude: 51.5287352,
longitude: -0.381773,
unit_system: {
length: "km",
mass: "kg",
temperature: "°C",
volume: "L",
},
components: [],
time_zone: "America/Los_Angeles",
config_dir: "/config",
version: "DEMO",
whitelist_external_dirs: [],
};

View File

@ -0,0 +1,96 @@
import { Panels } from "../types";
export const demoPanels: Panels = {
lovelace: {
component_name: "lovelace",
icon: null,
title: null,
config: { mode: "storage" },
url_path: "lovelace",
},
"dev-state": {
component_name: "dev-state",
icon: null,
title: null,
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,
title: null,
config: null,
url_path: "dev-event",
},
"dev-template": {
component_name: "dev-template",
icon: null,
title: null,
config: null,
url_path: "dev-template",
},
profile: {
component_name: "profile",
icon: null,
title: null,
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,
title: null,
config: null,
url_path: "dev-info",
},
"dev-mqtt": {
component_name: "dev-mqtt",
icon: null,
title: null,
config: null,
url_path: "dev-mqtt",
},
"dev-service": {
component_name: "dev-service",
icon: null,
title: null,
config: null,
url_path: "dev-service",
},
// Uncomment when we are ready to stub the history API
// history: {
// component_name: "history",
// icon: "hass:poll-box",
// title: "history",
// config: null,
// url_path: "history",
// },
map: {
component_name: "map",
icon: "hass:account-location",
title: "map",
config: null,
url_path: "map",
},
config: {
component_name: "config",
icon: "hass:settings",
title: "config",
config: null,
url_path: "config",
},
};

View File

@ -1,4 +1,4 @@
export default {
export const demoResources = {
en: {
"state.default.off": "Off",
"state.default.on": "On",

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +1,53 @@
import {
HassEntityAttributeBase,
HassEntities,
} from "home-assistant-js-websocket";
/* tslint:disable:max-classes-per-file */
const now = () => new Date().toISOString();
const randomTime = () =>
new Date(new Date().getTime() - Math.random() * 80 * 60 * 1000).toISOString();
/* eslint-disable no-unused-vars */
export class Entity {
public domain: string;
public objectId: string;
public entityId: string;
public lastChanged: string;
public lastUpdated: string;
public state: string;
public baseAttributes: HassEntityAttributeBase & { [key: string]: any };
public attributes: HassEntityAttributeBase & { [key: string]: any };
public hass?: any;
constructor(domain, objectId, state, baseAttributes) {
this.domain = domain;
this.objectId = objectId;
this.entityId = `${domain}.${objectId}`;
this.lastChanged = randomTime();
this.lastUpdated = randomTime();
this.state = state;
this.state = String(state);
// These are the attributes that we always write to the state machine
this.baseAttributes = baseAttributes;
this.attributes = baseAttributes;
}
async handleService(domain, service, data) {
public async handleService(domain, service, data: { [key: string]: any }) {
// tslint:disable-next-line
console.log(
`Unmocked service for ${this.entityId}: ${domain}/${service}`,
data
);
}
update(state, attributes = {}) {
public update(state, attributes = {}) {
this.state = state;
this.lastUpdated = now();
this.lastChanged =
state === this.state ? this.lastChanged : this.lastUpdated;
this.attributes = Object.assign({}, this.baseAttributes, attributes);
this.attributes = { ...this.baseAttributes, ...attributes };
// tslint:disable-next-line
console.log("update", this.entityId, this);
this.hass.updateStates({
@ -38,7 +55,7 @@ export class Entity {
});
}
toState() {
public toState() {
return {
entity_id: this.entityId,
state: this.state,
@ -50,21 +67,16 @@ export class Entity {
}
export class LightEntity extends Entity {
async handleService(domain, service, data) {
if (!["homeassistant", this.domain].includes(domain)) return;
public async handleService(domain, service, data) {
if (!["homeassistant", this.domain].includes(domain)) {
return;
}
if (service === "turn_on") {
// eslint-disable-next-line
// tslint:disable-next-line
let { brightness, hs_color, brightness_pct } = data;
// eslint-disable-next-line
brightness = (255 * brightness_pct) / 100;
this.update(
"on",
Object.assign(this.attributes, {
brightness,
hs_color,
})
);
this.update("on", { ...this.attributes, brightness, hs_color });
} else if (service === "turn_off") {
this.update("off");
} else if (service === "toggle") {
@ -77,9 +89,36 @@ export class LightEntity extends Entity {
}
}
export class SwitchEntity extends Entity {
public async handleService(domain, service, data) {
if (!["homeassistant", this.domain].includes(domain)) {
return;
}
if (service === "turn_on") {
this.update("on", this.attributes);
} else if (service === "turn_off") {
this.update("off", this.attributes);
} else if (service === "toggle") {
if (this.state === "on") {
this.handleService(domain, "turn_off", data);
} else {
this.handleService(domain, "turn_on", data);
}
}
}
}
export class LockEntity extends Entity {
async handleService(domain, service, data) {
if (domain !== this.domain) return;
public async handleService(
domain,
service,
// @ts-ignore
data
) {
if (domain !== this.domain) {
return;
}
if (service === "lock") {
this.update("locked");
@ -90,8 +129,15 @@ export class LockEntity extends Entity {
}
export class CoverEntity extends Entity {
async handleService(domain, service, data) {
if (domain !== this.domain) return;
public async handleService(
domain,
service,
// @ts-ignore
data
) {
if (domain !== this.domain) {
return;
}
if (service === "open_cover") {
this.update("open");
@ -102,23 +148,25 @@ export class CoverEntity extends Entity {
}
export class ClimateEntity extends Entity {
async handleService(domain, service, data) {
if (domain !== this.domain) return;
public async handleService(domain, service, data) {
if (domain !== this.domain) {
return;
}
if (service === "set_operation_mode") {
this.update(
data.operation_mode === "heat" ? "heat" : data.operation_mode,
Object.assign(this.attributes, {
operation_mode: data.operation_mode,
})
{ ...this.attributes, operation_mode: data.operation_mode }
);
}
}
}
export class GroupEntity extends Entity {
async handleService(domain, service, data) {
if (!["homeassistant", this.domain].includes(domain)) return;
public async handleService(domain, service, data) {
if (!["homeassistant", this.domain].includes(domain)) {
return;
}
await Promise.all(
this.attributes.entity_id.map((ent) => {
@ -133,11 +181,24 @@ export class GroupEntity extends Entity {
const TYPES = {
climate: ClimateEntity,
light: LightEntity,
lock: LockEntity,
cover: CoverEntity,
group: GroupEntity,
light: LightEntity,
lock: LockEntity,
switch: SwitchEntity,
};
export default (domain, objectId, state, baseAttributes = {}) =>
export const getEntity = (
domain,
objectId,
state,
baseAttributes = {}
): Entity =>
new (TYPES[domain] || Entity)(domain, objectId, state, baseAttributes);
export const convertEntities = (states: HassEntities): Entity[] =>
Object.keys(states).map((entId) => {
const stateObj = states[entId];
const [domain, objectId] = entId.split(".", 2);
return getEntity(domain, objectId, stateObj.state, stateObj.attributes);
});

View File

@ -0,0 +1,206 @@
import { fireEvent } from "../common/dom/fire_event";
import { demoConfig } from "./demo_config";
import { demoServices } from "./demo_services";
import { demoResources } from "./demo_resources";
import { demoPanels } from "./demo_panels";
import { getEntity, Entity } from "./entity";
import { HomeAssistant } from "../types";
import { HassEntities } from "home-assistant-js-websocket";
const ensureArray = <T>(val: T | T[]): T[] =>
Array.isArray(val) ? val : [val];
export interface MockHomeAssistant extends HomeAssistant {
mockEntities: any;
updateHass(obj: Partial<MockHomeAssistant>);
updateStates(newStates: HassEntities);
addEntities(entites: Entity | Entity[], replace?: boolean);
mockWS(type: string, callback: (msg: any) => any);
mockAPI(
path: string,
callback: (
method: string,
path: string,
parameters: { [key: string]: any }
) => any
);
}
export const provideHass = (
elements,
{ initialStates = {}, panelUrl = "" } = {}
): MockHomeAssistant => {
elements = ensureArray(elements);
const wsCommands = {};
const restResponses = {};
let hass: MockHomeAssistant;
const entities = {};
function updateHass(obj: Partial<MockHomeAssistant>) {
hass = { ...hass, ...obj };
elements.forEach((el) => {
el.hass = hass;
});
}
function updateStates(newStates: HassEntities) {
updateHass({
states: { ...hass.states, ...newStates },
});
}
function addEntities(newEntities, replace: boolean = false) {
const states = {};
ensureArray(newEntities).forEach((ent) => {
ent.hass = hass;
entities[ent.entityId] = ent;
states[ent.entityId] = ent.toState();
});
if (replace) {
updateHass({
states,
});
} else {
updateStates(states);
}
}
function mockUpdateStateAPI(
// @ts-ignore
method,
path,
parameters
) {
const [domain, objectId] = path.substr(7).split(".", 2);
if (!domain || !objectId) {
return;
}
addEntities(
getEntity(domain, objectId, parameters.state, parameters.attributes)
);
}
updateHass({
// Home Assistant properties
config: demoConfig,
services: demoServices,
language: "en",
resources: demoResources,
states: initialStates,
themes: {
default_theme: "default",
themes: {},
},
panelUrl: panelUrl || "lovelace",
panels: demoPanels,
connection: {
addEventListener: () => undefined,
removeEventListener: () => undefined,
sendMessagePromise: () =>
new Promise(() => {
/* we never resolve */
}),
subscribeEvents: async (
// @ts-ignore
callback,
event
) => {
// tslint:disable-next-line
console.log("subscribeEvents", event);
// tslint:disable-next-line
return () => console.log("unsubscribeEvents", event);
},
socket: {
readyState: WebSocket.OPEN,
},
} as any,
translationMetadata: {
fragments: [],
translations: {},
},
auth: {} as any,
connected: true,
dockedSidebar: false,
moreInfoEntityId: "",
user: {
credentials: [],
id: "abcd",
is_owner: true,
mfa_modules: [],
name: "Demo User",
},
fetchWithAuth: () => Promise.reject("Not implemented"),
// Mock properties
mockEntities: entities,
// Home Assistant functions
async callService(domain, service, data) {
fireEvent(elements[0], "hass-notification", {
message: `Called service ${domain}/${service}`,
});
if (data && "entity_id" in data) {
await Promise.all(
ensureArray(data.entity_id).map((ent) =>
entities[ent].handleService(domain, service, data)
)
);
} else {
// tslint:disable-next-line
console.log("unmocked callService", domain, service, data);
}
},
async callWS(msg) {
const callback = wsCommands[msg.type];
return callback
? callback(msg)
: Promise.reject({
code: "command_not_mocked",
message: `WS Command ${
msg.type
} is not implemented in provide_hass.`,
});
},
async sendWS(msg) {
const callback = wsCommands[msg.type];
if (callback) {
callback(msg);
} else {
// tslint:disable-next-line
console.error(`Unknown WS command: ${msg.type}`);
}
// tslint:disable-next-line
console.log("sendWS", msg);
},
async callApi(method, path, parameters) {
const callback =
path.substr(0, 7) === "states/"
? mockUpdateStateAPI
: restResponses[path];
return callback
? callback(method, path, parameters)
: Promise.reject(`API Mock for ${path} is not implemented`);
},
// Mock functions
updateHass,
updateStates,
addEntities,
mockWS(type, callback) {
wsCommands[type] = callback;
},
mockAPI(path, callback) {
restResponses[path] = callback;
},
} as MockHomeAssistant);
// @ts-ignore
return hass;
};

View File

@ -28,7 +28,7 @@ LitElement.prototype.html = litHtml;
const ext = (baseClass, mixins) =>
mixins.reduceRight((base, mixin) => mixin(base), baseClass);
class HomeAssistant extends ext(PolymerElement, [
export class HomeAssistant extends ext(PolymerElement, [
AuthMixin,
ThemesMixin,
TranslationsMixin,
@ -42,7 +42,10 @@ class HomeAssistant extends ext(PolymerElement, [
]) {
static get template() {
return html`
<app-location route="{{route}}"></app-location>
<app-location
route="{{route}}"
use-hash-as-path="[[_useHashAsPath]]"
></app-location>
<app-route
route="{{route}}"
pattern="/:panel"
@ -102,6 +105,10 @@ class HomeAssistant extends ext(PolymerElement, [
);
}
get _useHashAsPath() {
return __DEMO__;
}
panelUrlChanged(newPanelUrl) {
super.panelUrlChanged(newPanelUrl);
this._updateHass({ panelUrl: newPanelUrl });

View File

@ -135,7 +135,7 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
connectedCallback() {
super.connectedCallback();
if (document.location.pathname === "/") {
if (this.route.prefix === "") {
this.navigate(`/${localStorage.defaultPage || DEFAULT_PANEL}`, true);
}
}

View File

@ -217,7 +217,9 @@ class PartialPanelResolver extends NavigateMixin(PolymerElement) {
);
this._state = "loaded";
},
() => {
(err) => {
// eslint-disable-next-line
console.error("Error loading panel", err);
this._state = "error";
}
);

View File

@ -183,13 +183,18 @@ const generateViewConfig = (
);
});
return {
const view: LovelaceViewConfig = {
path,
title,
icon,
badges,
cards,
};
if (icon) {
view.icon = icon;
}
return view;
};
export const generateLovelaceConfig = (
@ -263,6 +268,12 @@ export const generateLovelaceConfig = (
}
}
if (__DEMO__) {
views[0].cards!.unshift({
type: "custom:ha-demo-card",
});
}
return {
title,
views,

View File

@ -68,6 +68,24 @@ export class HUIView extends hassLocalizeLitMixin(LitElement) {
this._badges = [];
}
// Public to make demo happy
public createCardElement(cardConfig: LovelaceCardConfig) {
const element = createCardElement(cardConfig) as LovelaceCard;
element.hass = this.hass;
element.addEventListener(
"ll-rebuild",
(ev) => {
// In edit mode let it go to hui-root and rebuild whole view.
if (!this.lovelace!.editMode) {
ev.stopPropagation();
this._rebuildCard(element, cardConfig);
}
},
{ once: true }
);
return element;
}
protected render(): TemplateResult | void {
return html`
${this.renderStyles()}
@ -240,7 +258,7 @@ export class HUIView extends hassLocalizeLitMixin(LitElement) {
const elements: LovelaceCard[] = [];
const elementsToAppend: HTMLElement[] = [];
config.cards.forEach((cardConfig, cardIndex) => {
const element = this._createCardElement(cardConfig);
const element = this.createCardElement(cardConfig);
elements.push(element);
if (!this.lovelace!.editMode) {
@ -288,28 +306,11 @@ export class HUIView extends hassLocalizeLitMixin(LitElement) {
}
}
private _createCardElement(cardConfig: LovelaceCardConfig) {
const element = createCardElement(cardConfig) as LovelaceCard;
element.hass = this.hass;
element.addEventListener(
"ll-rebuild",
(ev) => {
// In edit mode let it go to hui-root and rebuild whole view.
if (!this.lovelace!.editMode) {
ev.stopPropagation();
this._rebuildCard(element, cardConfig);
}
},
{ once: true }
);
return element;
}
private _rebuildCard(
cardElToReplace: LovelaceCard,
config: LovelaceCardConfig
): void {
const newCardEl = this._createCardElement(config);
const newCardEl = this.createCardElement(config);
cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace);
this._cards = this._cards!.map((curCardEl) =>
curCardEl === cardElToReplace ? newCardEl : curCardEl

View File

@ -11,5 +11,8 @@ declare global {
entityId: string;
};
"location-changed": undefined;
"hass-notification": {
message: string;
};
}
}

View File

@ -11,6 +11,7 @@ import {
declare global {
var __DEV__: boolean;
var __DEMO__: boolean;
var __BUILD__: "latest" | "es5";
var __VERSION__: string;
}
@ -53,9 +54,9 @@ export interface Themes {
export interface Panel {
component_name: string;
config?: { [key: string]: any };
icon: string;
title: string;
config: { [key: string]: any } | null;
icon: string | null;
title: string | null;
url_path: string;
}

View File

@ -71,4 +71,6 @@ class HaUrlSync extends EventsMixin(PolymerElement) {
window.removeEventListener("popstate", this.popstateChangeListener);
}
}
customElements.define("ha-url-sync", HaUrlSync);
if (!__DEMO__) {
customElements.define("ha-url-sync", HaUrlSync);
}

View File

@ -2,7 +2,7 @@ const serviceWorkerUrl =
__BUILD__ === "latest" ? "/service_worker.js" : "/service_worker_es5";
export default () => {
if (!("serviceWorker" in navigator)) return;
if (!("serviceWorker" in navigator) || __DEMO__) return;
navigator.serviceWorker.register(serviceWorkerUrl).then((reg) => {
reg.addEventListener("updatefound", () => {

View File

@ -35,7 +35,7 @@ const generateJSPage = (entrypoint, latestBuild) => {
};
function createConfig(isProdBuild, latestBuild) {
let buildPath = latestBuild ? "hass_frontend/" : "hass_frontend_es5/";
const buildPath = latestBuild ? "hass_frontend/" : "hass_frontend_es5/";
const publicPath = latestBuild ? "/frontend_latest/" : "/frontend_es5/";
const entry = {
@ -104,6 +104,7 @@ function createConfig(isProdBuild, latestBuild) {
plugins: [
new webpack.DefinePlugin({
__DEV__: JSON.stringify(!isProdBuild),
__DEMO__: false,
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(VERSION),
__STATIC_PATH__: "/static/",

View File

@ -7356,9 +7356,9 @@ hoek@4.x.x:
integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==
home-assistant-js-websocket@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-3.2.4.tgz#0c4212e6ac57b60ed939aa420253994e4f9f0bef"
integrity sha512-DaHpWIjJFLwTWNbHeGSCEUsbeyLUWAyWUgsYkiVWxzbfm+vqC5YaLNRu+Ma64SQYh5yGSYr7h25p2hip1GvyhQ==
version "3.2.5"
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-3.2.5.tgz#ac4fa6a7cb5cb48efe2a49390cf24acb5439f51f"
integrity sha512-CRlq9WA1WGw9lVzouK4BxEGEP5JqWV2MBBZyiUPVgBLHPR9p3bJL/y+jNhnjqEyb8QNPVboGuAJ+Rylrl7o2dg==
home-or-tmp@^2.0.0:
version "2.0.0"