Compare commits

...
4 changed files with 407 additions and 0 deletions

No files matched your search

+1
View File
@@ -5,6 +5,7 @@ import "./workers";
import "./ha-embed-provider";
import "./ha-embed-card";
import "./ha-embed-section";
import "./ha-embed-view";
// For custom cards, which can't load them themselves. The built-in cards only
// load ha-list-item-button with the map card. The full app loads ha-select
// with the more info dialog.
+134
View File
@@ -0,0 +1,134 @@
import { css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import memoizeOne from "memoize-one";
import type {
LovelaceConfig,
LovelaceRawConfig,
} from "../../../src/data/lovelace/config/types";
import type { LovelaceViewRawConfig } from "../../../src/data/lovelace/config/view";
import "../../../src/panels/lovelace/views/hui-view";
import "../../../src/panels/lovelace/views/hui-view-background";
import "../../../src/panels/lovelace/views/hui-view-container";
import type { HomeAssistant } from "../../../src/types";
import {
EmbedLovelaceElement,
findViewIndex,
isDashboardConfig,
} from "./lovelace";
import type { ScriptObject } from "./script-data";
/**
* Renders a dashboard view, a view strategy, or one view of a dashboard or
* dashboard strategy, with the backend of a provider, see `EmbedElement`.
* `config` holds the view.
*/
@customElement("ha-embed-view")
export class HaEmbedView extends EmbedLovelaceElement<LovelaceViewRawConfig> {
/**
* A dashboard configuration or dashboard strategy, to show its `view`
* instead of `config`. Also read from the `dashboard` key of a script child,
* next to `view`.
*/
@property({ type: Object }) public dashboard?: LovelaceRawConfig;
private _viewDashboard = memoizeOne(
(view: LovelaceViewRawConfig): LovelaceConfig => ({ views: [view] })
);
protected get rawConfig() {
return this.dashboard ?? (this.config && this._viewDashboard(this.config));
}
// A view strategy and a dashboard strategy both have a `strategy` key, so
// a dashboard comes in its own key, which no view has
protected isConfig(data: ScriptObject): data is LovelaceViewRawConfig {
return !("dashboard" in data);
}
protected setScriptData(data: ScriptObject) {
if (!("dashboard" in data)) {
super.setScriptData(data);
return;
}
const { dashboard } = data;
if (
typeof dashboard === "object" &&
dashboard !== null &&
isDashboardConfig(dashboard)
) {
this.dashboard = dashboard;
}
if (
"view" in data &&
(typeof data.view === "string" || typeof data.view === "number")
) {
this.view ??= String(data.view);
}
}
// Nothing for a dashboard without views, such as an empty strategy result.
// The view's theme and background, or the dashboard's background, as in
// hui-root. Each view gets its own hui-view, as there, so that a view
// strategy still generating for the view before can't replace the cards of
// this one.
protected renderContent(hass: HomeAssistant) {
const views = this._lovelace?.config.views;
if (!this._lovelace || !views?.length) {
return undefined;
}
const index = findViewIndex(views, this.view);
const viewConfig = views[index];
const background =
viewConfig.background || this._lovelace.config.background;
return html`<hui-view-container .hass=${hass} .theme=${viewConfig.theme}>
<hui-view-background .hass=${hass} .background=${background}>
</hui-view-background>
${keyed(
viewConfig,
html`<hui-view
.hass=${hass}
.lovelace=${this._lovelace}
.index=${index}
.narrow=${this._narrow.value ?? false}
></hui-view>`
)}
</hui-view-container>`;
}
// A fixed background stays inside the element. With a height set on the
// element, a panel view fills it, and other views keep their own height.
static styles = [
EmbedLovelaceElement.styles,
css`
:host {
contain: layout paint;
}
.theme,
hui-view-container,
hui-view {
display: block;
height: 100%;
}
/* The background sits below the view, above the element's own */
hui-view-container {
position: relative;
isolation: isolate;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-embed-view": HaEmbedView;
}
}
+178
View File
@@ -0,0 +1,178 @@
import { ResizeController } from "@lit-labs/observers/resize-controller";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css } from "lit";
import { property, state } from "lit/decorators";
import { normalizeLovelaceConfig } from "../../../src/data/lovelace/config/normalize";
import type { LovelaceRawConfig } from "../../../src/data/lovelace/config/types";
import { isStrategyDashboard } from "../../../src/data/lovelace/config/types";
import type { LovelaceViewRawConfig } from "../../../src/data/lovelace/config/view";
import {
checkStrategyShouldRegenerate,
generateLovelaceDashboardStrategy,
} from "../../../src/panels/lovelace/strategies/get-strategy";
import type { Lovelace } from "../../../src/panels/lovelace/types";
import type { HomeAssistant } from "../../../src/types";
import { EmbedElement } from "./embed-element";
import type { ScriptObject } from "./script-data";
import { embedElementStyles } from "./theme-styles";
// The width below which the app is narrow, see home-assistant-main
const NARROW_WIDTH = 870;
/** The index of the view with this path or index, if there is one. */
const viewIndex = (
views: LovelaceViewRawConfig[],
view: string
): number | undefined => {
const byPath = views.findIndex((item) => item.path === view);
if (byPath !== -1) {
return byPath;
}
const index = Number(view);
return Number.isInteger(index) && index >= 0 && index < views.length
? index
: undefined;
};
/** Whether script data is a dashboard: its views, or a strategy for them. */
export const isDashboardConfig = (
data: ScriptObject
): data is LovelaceRawConfig => "views" in data || "strategy" in data;
/** The index of the view with this path or index, else the first view. */
export const findViewIndex = (
views: LovelaceViewRawConfig[],
view?: string
): number => (view === undefined ? 0 : (viewIndex(views, view) ?? 0));
/**
* The base of the embed elements that render a dashboard, or one of its
* views. It generates dashboard strategies, again when the registries they
* depend on change, and holds a dashboard that can't be edited or saved.
*/
export abstract class EmbedLovelaceElement<
Config extends ScriptObject,
> extends EmbedElement<Config> {
/** The path or index of the view to show. Without it, the first view. */
@property() public view?: string;
@state() protected _lovelace?: Lovelace;
// Narrow like the app at the same width, measured on the element because
// it rarely takes the width of the page
protected _narrow = new ResizeController(this, {
callback: (entries) =>
(entries[0]?.contentRect.width ?? NARROW_WIDTH) < NARROW_WIDTH,
});
// The configuration of the dashboard in `_lovelace`, or being generated
private _rawConfig?: LovelaceRawConfig;
// Counts the generations started, so that only the latest one is shown.
// Generating again for changed registries keeps the same configuration.
private _generation = 0;
// The backend at the last update, to tell what a strategy depends on changed
private _previousHass?: HomeAssistant;
/** The dashboard to render. The same object until it changes. */
protected abstract get rawConfig(): LovelaceRawConfig | undefined;
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
const raw = this.rawConfig;
const oldHass = this._previousHass;
this._previousHass = this._hass;
// Without a configuration, nothing is shown, and a generation still
// running is dropped
if (!raw) {
if (this._rawConfig) {
this._rawConfig = undefined;
this._lovelace = undefined;
this._generation++;
}
return;
}
if (!this._hass) {
return;
}
// A strategy is only asked whether to generate again when the backend
// changed, as in ha-panel-lovelace, not when the generated dashboard
// arrives
if (
raw !== this._rawConfig ||
(oldHass &&
oldHass !== this._hass &&
isStrategyDashboard(raw) &&
this._hass.config.state === "RUNNING" &&
checkStrategyShouldRegenerate(
"dashboard",
raw.strategy,
oldHass,
this._hass
))
) {
this._rawConfig = raw;
this._generate(raw, this._hass, ++this._generation);
}
}
private async _generate(
raw: LovelaceRawConfig,
hass: HomeAssistant,
generation: number
) {
const generated = isStrategyDashboard(raw)
? await generateLovelaceDashboardStrategy(raw, hass)
: raw;
const config = normalizeLovelaceConfig(generated);
// A newer generation has started. A generated dashboard has its views,
// rather than a strategy.
if (generation !== this._generation || isStrategyDashboard(config)) {
return;
}
this._lovelace = {
config,
rawConfig: raw,
editMode: false,
urlPath: null,
mode: "generated",
locale: hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
// The background of a dashboard, and no insets for a notch or home bar
static styles: CSSResultGroup = [
embedElementStyles,
css`
:host {
display: block;
--app-safe-area-inset-top: 0px;
--app-safe-area-inset-bottom: 0px;
--app-safe-area-inset-left: 0px;
--app-safe-area-inset-right: 0px;
}
.theme {
position: relative;
background: var(--primary-background-color);
}
`,
];
}
+94
View File
@@ -119,6 +119,10 @@
<code>&lt;ha-embed-section&gt;</code> renders one section, or a
section strategy.
</li>
<li>
<code>&lt;ha-embed-view&gt;</code> renders one view, a view strategy,
or one view of a dashboard.
</li>
</ul>
<p>
All cards in the same provider share their entities. When you change
@@ -403,6 +407,61 @@
</div>
</ha-embed-provider>
<h2>Views</h2>
<p>
<code>&lt;ha-embed-view&gt;</code> renders a view, with its badges and
layout. A <code>strategy</code> generates the view. Give the component
the width the view needs: masonry and sidebar views pick their columns
from the width of the page, not the component.
</p>
<pre><code>&lt;ha-embed-view&gt;
&lt;script type="text/yaml"&gt;
type: sections
badges:
- entity: sensor.outside_temperature
sections:
- cards: …
&lt;/script&gt;
&lt;/ha-embed-view&gt;</code></pre>
<p>
To show one view of a dashboard, or of a dashboard strategy, put the
dashboard in <code>dashboard</code> and the path or index of the view
in <code>view</code>. Without <code>view</code>, the first view shows.
</p>
<pre><code>&lt;ha-embed-view&gt;
&lt;script type="text/yaml"&gt;
dashboard:
strategy:
type: custom:my-dashboard
view: overview
&lt;/script&gt;
&lt;/ha-embed-view&gt;</code></pre>
<ha-embed-view provider="shared">
<!-- htmlmin:ignore --><script type="text/yaml">
type: sections
max_columns: 2
badges:
- entity: sensor.outside_temperature
sections:
- cards:
- type: heading
heading: Living room
- type: tile
entity: light.living_room
tap_action:
action: toggle
- type: tile
entity: switch.coffee_maker
tap_action:
action: toggle
- cards:
- type: heading
heading: Hallway
- type: thermostat
entity: climate.hallway
</script><!-- htmlmin:ignore -->
</ha-embed-view>
<h2>Light and dark mode</h2>
<p>
The cards follow the light or dark mode of the browser. To choose the
@@ -787,6 +846,41 @@ customElements.define("embed-demo-custom-card", EmbedDemoCustomCard);</code></pr
</td>
</tr>
</table>
<h3><code>&lt;ha-embed-view&gt;</code></h3>
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td><code>config</code></td>
<td>Attribute and property</td>
<td>
The view configuration or view strategy, the same as in a
dashboard. A JSON or YAML script child sets the first
configuration.
</td>
</tr>
<tr>
<td><code>dashboard</code></td>
<td>Attribute and property</td>
<td>
A dashboard configuration or dashboard strategy, to show one of its
views instead of <code>config</code>. A script child with a
<code>dashboard</code> key, and optionally <code>view</code>, sets
it.
</td>
</tr>
<tr>
<td><code>view</code></td>
<td>Attribute and property</td>
<td>
The path or index of the view of <code>dashboard</code> to show.
Without it, the first view.
</td>
</tr>
</table>
<p>
The mocked backend knows the actions of lights, switches, climate,
covers, fans, locks, media players, and more. Actions that open dialogs,