Compare commits

..

5 Commits

Author SHA1 Message Date
Petar Petrov
1a4a6e60fb clean up 2025-09-30 09:35:36 +03:00
Petar Petrov
d377275bff clean up 2025-09-30 08:50:41 +03:00
Petar Petrov
65d15da469 slots 2025-09-30 08:44:59 +03:00
Petar Petrov
903ab67604 Use hui-tile in hui-home-summary-card 2025-09-29 20:55:37 +03:00
Petar Petrov
948b020b7c Reusable hui-tile component 2025-09-29 20:49:54 +03:00
26 changed files with 404 additions and 1533 deletions

View File

@@ -122,22 +122,3 @@ export const generateEntityFilter = (
return true;
};
};
export const findEntities = (
entities: string[],
filters: EntityFilterFunc[]
): string[] => {
const seen = new Set<string>();
const results: string[] = [];
for (const filter of filters) {
for (const entity of entities) {
if (filter(entity) && !seen.has(entity)) {
seen.add(entity);
results.push(entity);
}
}
}
return results;
};

View File

@@ -32,10 +32,7 @@ export class HaIconOverflowMenu extends LitElement {
@property({ type: Boolean }) public narrow = false;
protected render(): TemplateResult | typeof nothing {
if (this.items.length === 0) {
return nothing;
}
protected render(): TemplateResult {
return html`
${this.narrow
? html` <!-- Collapsed representation for small screens -->

View File

@@ -32,9 +32,6 @@ const COMPONENTS = {
todo: () => import("../panels/todo/ha-panel-todo"),
"media-browser": () =>
import("../panels/media-browser/ha-panel-media-browser"),
lights: () => import("../panels/lights/ha-panel-lights"),
security: () => import("../panels/security/ha-panel-security"),
climate: () => import("../panels/climate/ha-panel-climate"),
};
@customElement("partial-panel-resolver")

View File

@@ -1,214 +0,0 @@
import { mdiCog } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { goBack, navigate } from "../../common/navigate";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
const CLIMATE_LOVELACE_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "climate",
},
},
],
};
@customElement("ha-panel-climate")
class PanelClimate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() private _viewIndex = 0;
@state() private _lovelace?: Lovelace;
@state() private _searchParms = new URLSearchParams(window.location.search);
public firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
}
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
}
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass?.locale !== this.hass.locale) {
this._setLovelace();
}
}
private _back(ev) {
ev.stopPropagation();
goBack();
}
protected render(): TemplateResult {
return html`
<div class="header">
<div class="toolbar">
${this._searchParms.has("historyBack")
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
<div class="main-title">${this.hass.localize("panel.climate")}</div>
${this.hass.user?.is_admin
? html`<ha-icon-button
@click=${this._navigateConfig}
.path=${mdiCog}
title=${this.hass!.localize("ui.panel.energy.configure")}
>
</ha-icon-button>`
: nothing}
</div>
</div>
<hui-view-container .hass=${this.hass}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view>
</hui-view-container>
`;
}
private _setLovelace() {
this._lovelace = {
config: CLIMATE_LOVELACE_CONFIG,
rawConfig: CLIMATE_LOVELACE_CONFIG,
editMode: false,
urlPath: "climate",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
private _navigateConfig(ev) {
ev.stopPropagation();
navigate("/config/climate?historyBack=1");
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
.header {
background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
position: fixed;
top: 0;
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-right,
0px
)
);
padding-top: var(--safe-area-inset-top);
z-index: 4;
transition: box-shadow 200ms linear;
display: flex;
flex-direction: row;
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
backdrop-filter: var(--app-header-backdrop-filter, none);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
}
:host([narrow]) .header {
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-left,
0px
) - var(--safe-area-inset-right, 0px)
);
padding-left: var(--safe-area-inset-left);
}
:host([scrolled]) .header {
box-shadow: var(
--mdc-top-app-bar-fixed-box-shadow,
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
);
}
.toolbar {
height: var(--header-height);
display: flex;
flex: 1;
align-items: center;
font-size: var(--ha-font-size-xl);
padding: 0px 12px;
font-weight: var(--ha-font-weight-normal);
box-sizing: border-box;
}
:host([narrow]) .toolbar {
padding: 0 4px;
}
.main-title {
margin: var(--margin-title);
line-height: var(--ha-line-height-normal);
flex-grow: 1;
}
hui-view-container {
position: relative;
display: flex;
min-height: 100vh;
box-sizing: border-box;
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
padding-right: var(--safe-area-inset-right);
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
}
:host([narrow]) hui-view-container {
padding-left: var(--safe-area-inset-left);
padding-inline-start: var(--safe-area-inset-left);
}
hui-view {
flex: 1 1 100%;
max-width: 100%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-panel-climate": PanelClimate;
}
}

View File

@@ -1,203 +0,0 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import {
findEntities,
generateEntityFilter,
type EntityFilter,
} from "../../../common/entity/entity_filter";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import {
computeAreaTileCardConfig,
getAreas,
getFloors,
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
export interface ClimateViewStrategyConfig {
type: "climate";
}
export const climateEntityFilters: EntityFilter[] = [
{ domain: "climate", entity_category: "none" },
{ domain: "humidifier", entity_category: "none" },
{ domain: "fan", entity_category: "none" },
{ domain: "water_heater", entity_category: "none" },
{
domain: "cover",
device_class: [
"awning",
"blind",
"curtain",
"shade",
"shutter",
"window",
"none",
],
entity_category: "none",
},
{
domain: "binary_sensor",
device_class: ["window"],
entity_category: "none",
},
];
const processAreasForClimate = (
areaIds: string[],
hass: HomeAssistant,
entities: string[]
): LovelaceCardConfig[] => {
const cards: LovelaceCardConfig[] = [];
const computeTileCard = computeAreaTileCardConfig(hass, "", true);
for (const areaId of areaIds) {
const area = hass.areas[areaId];
if (!area) continue;
const areaFilter = generateEntityFilter(hass, {
area: area.area_id,
});
const areaClimateEntities = entities.filter(areaFilter);
const areaCards: LovelaceCardConfig[] = [];
// Add temperature and humidity sensors with trend graphs for areas
const temperatureEntityId = area.temperature_entity_id;
if (temperatureEntityId && hass.states[temperatureEntityId]) {
areaCards.push({
...computeTileCard(temperatureEntityId),
features: [{ type: "trend-graph" }],
});
}
const humidityEntityId = area.humidity_entity_id;
if (humidityEntityId && hass.states[humidityEntityId]) {
areaCards.push({
...computeTileCard(humidityEntityId),
features: [{ type: "trend-graph" }],
});
}
// Add other climate entities
for (const entityId of areaClimateEntities) {
// Skip if already added as temperature/humidity sensor
if (entityId === temperatureEntityId || entityId === humidityEntityId) {
continue;
}
const state = hass.states[entityId];
if (
state?.attributes.device_class === "temperature" ||
state?.attributes.device_class === "humidity"
) {
areaCards.push({
...computeTileCard(entityId),
features: [{ type: "trend-graph" }],
});
} else {
areaCards.push(computeTileCard(entityId));
}
}
if (areaCards.length > 0) {
cards.push({
heading_style: "subtitle",
type: "heading",
heading: area.name,
});
cards.push(...areaCards);
}
}
return cards;
};
@customElement("climate-view-strategy")
export class ClimateViewStrategy extends ReactiveElement {
static async generate(
_config: ClimateViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const areas = getAreas(hass.areas);
const floors = getFloors(hass.floors);
const home = getHomeStructure(floors, areas);
const sections: LovelaceSectionRawConfig[] = [];
const allEntities = Object.keys(hass.states);
const climateFilters = climateEntityFilters.map((filter) =>
generateEntityFilter(hass, filter)
);
const entities = findEntities(allEntities, climateFilters);
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
// Process floors
for (const floorStructure of home.floors) {
const floorId = floorStructure.id;
const areaIds = floorStructure.areas;
const floor = hass.floors[floorId];
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
floorCount > 1
? floor.name
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
},
],
};
const areaCards = processAreasForClimate(areaIds, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
sections.push(section);
}
}
// Process unassigned areas
if (home.areas.length > 0) {
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
floorCount > 1
? hass.localize("ui.panel.lovelace.strategy.home.other_areas")
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
},
],
};
const areaCards = processAreasForClimate(home.areas, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
sections.push(section);
}
}
return {
type: "sections",
max_columns: 2,
sections: sections || [],
};
}
}
declare global {
interface HTMLElementTagNameMap {
"climate-view-strategy": ClimateViewStrategy;
}
}

View File

@@ -1,42 +0,0 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
@customElement("ha-config-climate")
class HaConfigClimate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _searchParms = new URLSearchParams(window.location.search);
protected render(): TemplateResult {
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config/lovelace/dashboards"}
.header=${"Climate"}
>
<div class="container"></div>
</hass-subpage>
`;
}
static get styles(): CSSResultGroup {
return [haStyle, css``];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-climate": HaConfigClimate;
}
}

View File

@@ -577,10 +577,6 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
load: () =>
import("./application_credentials/ha-config-application-credentials"),
},
climate: {
tag: "ha-config-climate",
load: () => import("./climate/ha-config-climate"),
},
},
};

View File

@@ -238,17 +238,13 @@ export class HaConfigLovelaceDashboards extends LitElement {
.hass=${this.hass}
narrow
.items=${[
...(this._canEdit(dashboard.url_path)
? [
{
path: mdiPencil,
label: this.hass.localize(
"ui.panel.config.lovelace.dashboards.picker.edit"
),
action: () => this._handleEdit(dashboard),
},
]
: []),
{
path: mdiPencil,
label: this.hass.localize(
"ui.panel.config.lovelace.dashboards.picker.edit"
),
action: () => this._handleEdit(dashboard),
},
...(this._canDelete(dashboard.url_path)
? [
{
@@ -298,49 +294,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
mode: "storage",
url_path: "energy",
filename: "",
iconColor: "var(--orange-color)",
default: false,
require_admin: false,
});
}
if (this.hass.panels.lights) {
result.push({
icon: "mdi:lamps",
title: this.hass.localize("panel.lights"),
show_in_sidebar: false,
mode: "storage",
url_path: "lights",
filename: "",
iconColor: "var(--amber-color)",
default: false,
require_admin: false,
});
}
if (this.hass.panels.security) {
result.push({
icon: "mdi:security",
title: this.hass.localize("panel.security"),
show_in_sidebar: false,
mode: "storage",
url_path: "security",
filename: "",
iconColor: "var(--blue-grey-color)",
default: false,
require_admin: false,
});
}
if (this.hass.panels.climate) {
result.push({
icon: "mdi:home-thermometer",
title: this.hass.localize("panel.climate"),
show_in_sidebar: false,
mode: "storage",
url_path: "climate",
filename: "",
iconColor: "var(--deep-orange-color)",
iconColor: "var(--label-badge-yellow)",
default: false,
require_admin: false,
});
@@ -438,28 +392,12 @@ export class HaConfigLovelaceDashboards extends LitElement {
navigate("/config/energy");
return;
}
if (urlPath === "climate") {
navigate("/config/climate");
return;
}
const dashboard = this._dashboards.find((res) => res.url_path === urlPath);
this._openDetailDialog(dashboard, urlPath);
}
private _canDelete(urlPath: string) {
if (
urlPath === "lovelace" ||
urlPath === "energy" ||
urlPath === "lights" ||
urlPath === "security"
) {
return false;
}
return true;
}
private _canEdit(urlPath: string) {
if (urlPath === "lights" || urlPath === "security") {
if (urlPath === "lovelace" || urlPath === "energy") {
return false;
}
return true;

View File

@@ -1,200 +0,0 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { goBack } from "../../common/navigate";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
const LIGHTS_LOVELACE_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "lights",
},
},
],
};
@customElement("ha-panel-lights")
class PanelLights extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() private _viewIndex = 0;
@state() private _lovelace?: Lovelace;
@state() private _searchParms = new URLSearchParams(window.location.search);
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
}
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass?.locale !== this.hass.locale) {
this._setLovelace();
}
}
private _back(ev) {
ev.stopPropagation();
goBack();
}
protected render(): TemplateResult {
return html`
<div class="header">
<div class="toolbar">
${this._searchParms.has("historyBack")
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
${!this.narrow
? html`<div class="main-title">
${this.hass.localize("panel.lights")}
</div>`
: nothing}
</div>
</div>
<hui-view-container .hass=${this.hass}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view>
</hui-view-container>
`;
}
private _setLovelace() {
this._lovelace = {
config: LIGHTS_LOVELACE_CONFIG,
rawConfig: LIGHTS_LOVELACE_CONFIG,
editMode: false,
urlPath: "lights",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
.header {
background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
position: fixed;
top: 0;
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-right,
0px
)
);
padding-top: var(--safe-area-inset-top);
z-index: 4;
transition: box-shadow 200ms linear;
display: flex;
flex-direction: row;
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
backdrop-filter: var(--app-header-backdrop-filter, none);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
}
:host([narrow]) .header {
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-left,
0px
) - var(--safe-area-inset-right, 0px)
);
padding-left: var(--safe-area-inset-left);
}
:host([scrolled]) .header {
box-shadow: var(
--mdc-top-app-bar-fixed-box-shadow,
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
);
}
.toolbar {
height: var(--header-height);
display: flex;
flex: 1;
align-items: center;
font-size: var(--ha-font-size-xl);
padding: 0px 12px;
font-weight: var(--ha-font-weight-normal);
box-sizing: border-box;
}
:host([narrow]) .toolbar {
padding: 0 4px;
}
.main-title {
margin: var(--margin-title);
line-height: var(--ha-line-height-normal);
flex-grow: 1;
}
hui-view-container {
position: relative;
display: flex;
min-height: 100vh;
box-sizing: border-box;
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
padding-right: var(--safe-area-inset-right);
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
}
:host([narrow]) hui-view-container {
padding-left: var(--safe-area-inset-left);
padding-inline-start: var(--safe-area-inset-left);
}
hui-view {
flex: 1 1 100%;
max-width: 100%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-panel-lights": PanelLights;
}
}

View File

@@ -1,27 +1,19 @@
import { css, html, LitElement, nothing } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import { computeDomain } from "../../../common/entity/compute_domain";
import {
findEntities,
generateEntityFilter,
} from "../../../common/entity/entity_filter";
import { generateEntityFilter } from "../../../common/entity/entity_filter";
import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import "../../../components/ha-ripple";
import "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-info";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import "../../../state-display/state-display";
import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import "../components/hui-tile";
import {
findEntities,
getSummaryLabel,
HOME_SUMMARIES_FILTERS,
HOME_SUMMARIES_ICONS,
@@ -231,140 +223,32 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
return nothing;
}
const contentClasses = { vertical: Boolean(this._config.vertical) };
const color = computeCssColor(COLORS[this._config.summary]);
const style = {
"--tile-color": color,
};
const secondary = this._computeSummaryState();
const label = getSummaryLabel(this.hass.localize, this._config.summary);
const icon = HOME_SUMMARIES_ICONS[this._config.summary];
const color = computeCssColor(COLORS[this._config.summary]);
const secondary = this._computeSummaryState();
return html`
<ha-card style=${styleMap(style)}>
<div
class="background"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
role=${ifDefined(this._hasCardAction ? "button" : undefined)}
tabindex=${ifDefined(this._hasCardAction ? "0" : undefined)}
aria-labelledby="info"
>
<ha-ripple .disabled=${!this._hasCardAction}></ha-ripple>
</div>
<div class="container">
<div class="content ${classMap(contentClasses)}">
<ha-tile-icon>
<ha-icon slot="icon" .icon=${icon}></ha-icon>
</ha-tile-icon>
<ha-tile-info
id="info"
.primary=${label}
.secondary=${secondary}
></ha-tile-info>
</div>
</div>
</ha-card>
<hui-tile
name=${label}
?vertical=${this._config.vertical}
color=${color}
.hasCardAction=${this._hasCardAction}
.onAction=${this._handleAction}
.tapAction=${this._config.tap_action}
.holdAction=${this._config.hold_action}
.doubleTapAction=${this._config.double_tap_action}
>
<ha-tile-icon slot="icon">
<ha-icon slot="icon" .icon=${icon}></ha-icon>
</ha-tile-icon>
<ha-tile-info slot="info" id="info">
<span slot="primary" class="primary">${label}</span>
<span slot="secondary">${secondary}</span>
</ha-tile-info>
</hui-tile>
`;
}
static styles = css`
:host {
--tile-color: var(--state-inactive-color);
-webkit-tap-highlight-color: transparent;
}
ha-card:has(.background:focus-visible) {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--tile-color);
border-color: var(--tile-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
ha-card {
--ha-ripple-color: var(--tile-color);
--ha-ripple-hover-opacity: 0.04;
--ha-ripple-pressed-opacity: 0.12;
height: 100%;
transition:
box-shadow 180ms ease-in-out,
border-color 180ms ease-in-out;
display: flex;
flex-direction: column;
justify-content: space-between;
}
ha-card.active {
--tile-color: var(--state-icon-color);
}
[role="button"] {
cursor: pointer;
pointer-events: auto;
}
[role="button"]:focus {
outline: none;
}
.background {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
border-radius: var(--ha-card-border-radius, 12px);
margin: calc(-1 * var(--ha-card-border-width, 1px));
overflow: hidden;
}
.container {
margin: calc(-1 * var(--ha-card-border-width, 1px));
display: flex;
flex-direction: column;
flex: 1;
}
.container.horizontal {
flex-direction: row;
}
.content {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
padding: 10px;
flex: 1;
min-width: 0;
box-sizing: border-box;
pointer-events: none;
gap: 10px;
}
.vertical {
flex-direction: column;
text-align: center;
justify-content: center;
}
.vertical ha-tile-info {
width: 100%;
flex: none;
}
ha-tile-icon {
--tile-icon-color: var(--tile-color);
position: relative;
padding: 6px;
margin: -6px;
}
ha-tile-info {
position: relative;
min-width: 0;
transition: background-color 180ms ease-in-out;
box-sizing: border-box;
}
`;
}
declare global {

View File

@@ -3,7 +3,6 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
@@ -12,24 +11,15 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-card";
import "../../../components/ha-ripple";
import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon";
import "../../../components/tile/ha-tile-badge";
import "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-info";
import { cameraUrlWithWidthHeight } from "../../../data/camera";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import "../../../state-display/state-display";
import type { HomeAssistant } from "../../../types";
import "../card-features/hui-card-features";
import type { LovelaceCardFeatureContext } from "../card-features/types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import "../components/hui-tile";
import type {
LovelaceCard,
LovelaceCardEditor,
@@ -37,6 +27,7 @@ import type {
} from "../types";
import { renderTileBadge } from "./tile/badges/tile-badge";
import type { TileCardConfig } from "./types";
import { actionHandler } from "../common/directives/action-handler-directive";
export const getEntityDefaultTileIconAction = (entityId: string) => {
const domain = computeDomain(entityId);
@@ -253,10 +244,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
`;
}
const contentClasses = { vertical: Boolean(this._config.vertical) };
const name = this._config.name || computeStateName(stateObj);
const active = stateActive(stateObj);
const color = this._computeStateColor(stateObj, this._config.color);
const domain = computeDomain(stateObj.entity_id);
@@ -272,162 +260,69 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
</state-display>
`;
const style = {
"--tile-color": color,
};
const imageUrl = this._config.show_entity_picture
? this._getImageUrl(stateObj)
: undefined;
const featurePosition = this._featurePosition(this._config);
const features = this._displayedFeatures(this._config);
const containerOrientationClass =
featurePosition === "inline" ? "horizontal" : "";
return html`
<ha-card style=${styleMap(style)} class=${classMap({ active })}>
<div
class="background"
@action=${this._handleAction}
<hui-tile
?vertical=${this._config.vertical}
color=${ifDefined(color)}
.hasCardAction=${this._hasCardAction}
.hasIconAction=${this._hasIconAction}
.onAction=${this._handleAction}
.tapAction=${this._config.tap_action}
.holdAction=${this._config.hold_action}
.doubleTapAction=${this._config.double_tap_action}
.featurePosition=${this._featurePosition(this._config)}
>
<ha-tile-icon
slot="icon"
role=${ifDefined(this._hasIconAction ? "button" : undefined)}
tabindex=${ifDefined(this._hasIconAction ? "0" : undefined)}
@action=${this._handleIconAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
hasHold: hasAction(this._config!.icon_hold_action),
hasDoubleClick: hasAction(this._config!.icon_double_tap_action),
})}
role=${ifDefined(this._hasCardAction ? "button" : undefined)}
tabindex=${ifDefined(this._hasCardAction ? "0" : undefined)}
aria-labelledby="info"
.interactive=${this._hasIconAction}
.imageUrl=${imageUrl}
data-domain=${ifDefined(domain)}
data-state=${ifDefined(stateObj?.state)}
class=${classMap({ image: Boolean(imageUrl) })}
>
<ha-ripple .disabled=${!this._hasCardAction}></ha-ripple>
</div>
<div class="container ${containerOrientationClass}">
<div class="content ${classMap(contentClasses)}">
<ha-tile-icon
role=${ifDefined(this._hasIconAction ? "button" : undefined)}
tabindex=${ifDefined(this._hasIconAction ? "0" : undefined)}
@action=${this._handleIconAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.icon_hold_action),
hasDoubleClick: hasAction(this._config!.icon_double_tap_action),
})}
.interactive=${this._hasIconAction}
.imageUrl=${imageUrl}
data-domain=${ifDefined(domain)}
data-state=${ifDefined(stateObj?.state)}
class=${classMap({ image: Boolean(imageUrl) })}
>
<ha-state-icon
slot="icon"
.icon=${this._config.icon}
.stateObj=${stateObj}
.hass=${this.hass}
></ha-state-icon>
${renderTileBadge(stateObj, this.hass)}
</ha-tile-icon>
<ha-tile-info id="info">
<span slot="primary" class="primary">${name}</span>
${stateDisplay
? html`<span slot="secondary">${stateDisplay}</span>`
: nothing}
</ha-tile-info>
</div>
${features.length > 0
? html`
<hui-card-features
.hass=${this.hass}
.context=${this._featureContext}
.color=${this._config.color}
.features=${features}
></hui-card-features>
`
<ha-state-icon
slot="icon"
.icon=${this._config.icon}
.stateObj=${stateObj}
.hass=${this.hass}
></ha-state-icon>
${renderTileBadge(stateObj, this.hass)}
</ha-tile-icon>
<ha-tile-info slot="info" id="info">
<span slot="primary" class="primary">${name}</span>
${stateDisplay
? html`<span slot="secondary">${stateDisplay}</span>`
: nothing}
</div>
</ha-card>
</ha-tile-info>
${features.length
? html`
<hui-card-features
slot="features"
.hass=${this.hass}
.context=${this._featureContext}
.color=${this._config.color}
.features=${features}
></hui-card-features>
`
: nothing}
</hui-tile>
`;
}
static styles = css`
:host {
--tile-color: var(--state-inactive-color);
-webkit-tap-highlight-color: transparent;
}
ha-card:has(.background:focus-visible) {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--tile-color);
border-color: var(--tile-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
ha-card {
--ha-ripple-color: var(--tile-color);
--ha-ripple-hover-opacity: 0.04;
--ha-ripple-pressed-opacity: 0.12;
height: 100%;
transition:
box-shadow 180ms ease-in-out,
border-color 180ms ease-in-out;
display: flex;
flex-direction: column;
justify-content: space-between;
}
ha-card.active {
--tile-color: var(--state-icon-color);
}
[role="button"] {
cursor: pointer;
pointer-events: auto;
}
[role="button"]:focus {
outline: none;
}
.background {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
border-radius: var(--ha-card-border-radius, 12px);
margin: calc(-1 * var(--ha-card-border-width, 1px));
overflow: hidden;
}
.container {
margin: calc(-1 * var(--ha-card-border-width, 1px));
display: flex;
flex-direction: column;
flex: 1;
}
.container.horizontal {
flex-direction: row;
}
.content {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
padding: 10px;
flex: 1;
min-width: 0;
box-sizing: border-box;
pointer-events: none;
gap: 10px;
}
.vertical {
flex-direction: column;
text-align: center;
justify-content: center;
}
.vertical ha-tile-info {
width: 100%;
flex: none;
}
ha-tile-icon {
--tile-icon-color: var(--tile-color);
position: relative;
padding: 6px;
margin: -6px;
}
ha-tile-badge {
position: absolute;
top: 3px;
@@ -435,23 +330,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
inset-inline-end: 3px;
inset-inline-start: initial;
}
ha-tile-info {
position: relative;
min-width: 0;
transition: background-color 180ms ease-in-out;
box-sizing: border-box;
}
hui-card-features {
--feature-color: var(--tile-color);
padding: 0 12px 12px 12px;
}
.container.horizontal hui-card-features {
width: calc(50% - var(--column-gap, 0px) / 2 - 12px);
flex: none;
--feature-height: 36px;
padding: 0 12px;
padding-inline-start: 0;
}
ha-tile-icon[data-domain="alarm_control_panel"][data-state="pending"],
ha-tile-icon[data-domain="alarm_control_panel"][data-state="arming"],

View File

@@ -0,0 +1,181 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import "../../../components/ha-card";
import "../../../components/ha-ripple";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import type { LovelaceCardFeaturePosition } from "../card-features/types";
@customElement("hui-tile")
export class HuiTile extends LitElement {
@property({ type: Boolean }) public vertical = false;
@property() public color?: string;
@property({ attribute: false, type: Boolean }) public hasCardAction = false;
@property({ attribute: false, type: Boolean }) public hasIconAction = false;
@property({ attribute: false }) public onAction?: (
ev: ActionHandlerEvent
) => void;
@property({ attribute: false }) public tapAction?: any;
@property({ attribute: false }) public holdAction?: any;
@property({ attribute: false }) public doubleTapAction?: any;
@property({ attribute: false })
public featurePosition?: LovelaceCardFeaturePosition;
private _handleAction(ev: ActionHandlerEvent) {
this.onAction?.(ev);
}
protected render() {
const contentClasses = { vertical: Boolean(this.vertical) };
const style = {
"--tile-color": this.color,
};
const containerOrientationClass =
this.featurePosition === "inline" ? "horizontal" : "";
return html`
<ha-card style=${styleMap(style)}>
<div
class="background"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this.holdAction),
hasDoubleClick: hasAction(this.doubleTapAction),
})}
role=${ifDefined(this.hasCardAction ? "button" : undefined)}
tabindex=${ifDefined(this.hasCardAction ? "0" : undefined)}
aria-labelledby="info"
>
<ha-ripple .disabled=${!this.hasCardAction}></ha-ripple>
</div>
<div class="container ${containerOrientationClass}">
<div class="content ${classMap(contentClasses)}">
<slot name="icon"></slot>
<slot name="info"></slot>
</div>
<slot name="features"></slot>
</div>
</ha-card>
`;
}
static styles = css`
:host {
--tile-color: var(--state-inactive-color);
-webkit-tap-highlight-color: transparent;
}
ha-card:has(.background:focus-visible) {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--tile-color);
border-color: var(--tile-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
ha-card {
--ha-ripple-color: var(--tile-color);
--ha-ripple-hover-opacity: 0.04;
--ha-ripple-pressed-opacity: 0.12;
height: 100%;
transition:
box-shadow 180ms ease-in-out,
border-color 180ms ease-in-out;
display: flex;
flex-direction: column;
justify-content: space-between;
}
ha-card.active {
--tile-color: var(--state-icon-color);
}
[role="button"] {
cursor: pointer;
pointer-events: auto;
}
[role="button"]:focus {
outline: none;
}
.background {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
border-radius: var(--ha-card-border-radius, 12px);
margin: calc(-1 * var(--ha-card-border-width, 1px));
overflow: hidden;
}
.container {
margin: calc(-1 * var(--ha-card-border-width, 1px));
display: flex;
flex-direction: column;
flex: 1;
}
.container.horizontal {
flex-direction: row;
}
.content {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
padding: 10px;
flex: 1;
min-width: 0;
box-sizing: border-box;
pointer-events: none;
gap: 10px;
}
.vertical {
flex-direction: column;
text-align: center;
justify-content: center;
}
.vertical ::slotted(ha-tile-info) {
width: 100%;
flex: none;
}
::slotted(ha-tile-icon) {
--tile-icon-color: var(--tile-color);
position: relative;
padding: 6px;
margin: -6px;
}
::slotted(ha-tile-info) {
position: relative;
min-width: 0;
transition: background-color 180ms ease-in-out;
box-sizing: border-box;
}
::slotted(features) {
--feature-color: var(--tile-color);
padding: 0 12px 12px 12px;
}
.container.horizontal ::slotted(hui-card-features) {
width: calc(50% - var(--column-gap, 0px) / 2 - 12px);
flex: none;
--feature-height: 36px;
padding: 0 12px;
padding-inline-start: 0;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hui-tile": HuiTile;
}
}

View File

@@ -44,15 +44,12 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
area: () => import("./areas/area-view-strategy"),
"areas-overview": () => import("./areas/areas-overview-view-strategy"),
"home-main": () => import("./home/home-main-view-strategy"),
"home-lights": () => import("./home/home-lights-view-strategy"),
"home-climate": () => import("./home/home-climate-view-strategy"),
"home-security": () => import("./home/home-security-view-strategy"),
"home-media-players": () =>
import("./home/home-media-players-view-strategy"),
"home-area": () => import("./home/home-area-view-strategy"),
lights: () => import("../../lights/strategies/lights-view-strategy"),
security: () => import("../../security/strategies/security-view-strategy"),
climate: () => import("../../climate/strategies/climate-view-strategy"),
},
section: {
"common-controls": () =>

View File

@@ -6,6 +6,7 @@ interface HomeStructure {
id: string;
areas: string[];
}[];
areas: string[];
}

View File

@@ -1,8 +1,8 @@
import type { EntityFilter } from "../../../../../common/entity/entity_filter";
import type {
EntityFilter,
EntityFilterFunc,
} from "../../../../../common/entity/entity_filter";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import { climateEntityFilters } from "../../../../climate/strategies/climate-view-strategy";
import { lightEntityFilters } from "../../../../lights/strategies/lights-view-strategy";
import { securityEntityFilters } from "../../../../security/strategies/security-view-strategy";
export const HOME_SUMMARIES = [
"lights",
@@ -21,18 +21,97 @@ export const HOME_SUMMARIES_ICONS: Record<HomeSummary, string> = {
};
export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
lights: lightEntityFilters,
climate: climateEntityFilters,
security: securityEntityFilters,
lights: [{ domain: "light", entity_category: "none" }],
climate: [
{ domain: "climate", entity_category: "none" },
{ domain: "humidifier", entity_category: "none" },
{ domain: "fan", entity_category: "none" },
{ domain: "water_heater", entity_category: "none" },
{
domain: "cover",
device_class: [
"awning",
"blind",
"curtain",
"shade",
"shutter",
"window",
"none",
],
entity_category: "none",
},
{
domain: "binary_sensor",
device_class: ["window"],
entity_category: "none",
},
],
security: [
{
domain: "camera",
entity_category: "none",
},
{
domain: "alarm_control_panel",
entity_category: "none",
},
{
domain: "lock",
entity_category: "none",
},
{
domain: "cover",
device_class: ["door", "garage", "gate"],
entity_category: "none",
},
{
domain: "binary_sensor",
device_class: [
// Locks
"lock",
// Openings
"door",
"window",
"garage_door",
"opening",
// Safety
"carbon_monoxide",
"gas",
"moisture",
"safety",
"smoke",
"tamper",
],
entity_category: "none",
},
// We also want the tamper sensors when they are diagnostic
{
domain: "binary_sensor",
device_class: ["tamper"],
entity_category: "diagnostic",
},
],
media_players: [{ domain: "media_player", entity_category: "none" }],
};
export const getSummaryLabel = (
localize: LocalizeFunc,
summary: HomeSummary
) => {
if (summary === "lights" || summary === "climate" || summary === "security") {
return localize(`panel.${summary}`);
export const findEntities = (
entities: string[],
filters: EntityFilterFunc[]
): string[] => {
const seen = new Set<string>();
const results: string[] = [];
for (const filter of filters) {
for (const entity of entities) {
if (filter(entity) && !seen.has(entity)) {
seen.add(entity);
results.push(entity);
}
}
}
return localize(`ui.panel.lovelace.strategy.home.summary_list.${summary}`);
return results;
};
export const getSummaryLabel = (localize: LocalizeFunc, summary: HomeSummary) =>
localize(`ui.panel.lovelace.strategy.home.summary_list.${summary}`);

View File

@@ -2,10 +2,7 @@ import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
import {
findEntities,
generateEntityFilter,
} from "../../../../common/entity/entity_filter";
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import { clamp } from "../../../../common/number/clamp";
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
@@ -15,6 +12,7 @@ import type { HomeAssistant } from "../../../../types";
import type { HeadingCardConfig } from "../../cards/types";
import { computeAreaTileCardConfig } from "../areas/helpers/areas-strategy-helper";
import {
findEntities,
getSummaryLabel,
HOME_SUMMARIES,
HOME_SUMMARIES_FILTERS,
@@ -115,7 +113,7 @@ export class HomeAreaViewStrategy extends ReactiveElement {
computeHeadingCard(
getSummaryLabel(hass.localize, "lights"),
HOME_SUMMARIES_ICONS.lights,
"/lights?historyBack=1"
"lights"
),
...lights.map(computeTileCard),
],

View File

@@ -1,9 +1,6 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import {
findEntities,
generateEntityFilter,
} from "../../../../common/entity/entity_filter";
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
@@ -14,7 +11,7 @@ import {
getFloors,
} from "../areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "./helpers/home-structure";
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
import { findEntities, HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
export interface HomeClimateViewStrategyConfig {
type: "home-climate";

View File

@@ -62,6 +62,16 @@ export class HomeDashboardStrategy extends ReactiveElement {
};
});
const lightView = {
title: getSummaryLabel(hass.localize, "lights"),
path: "lights",
subview: true,
strategy: {
type: "home-lights",
},
icon: HOME_SUMMARIES_ICONS.lights,
} satisfies LovelaceViewRawConfig;
const climateView = {
title: getSummaryLabel(hass.localize, "climate"),
path: "climate",
@@ -103,6 +113,7 @@ export class HomeDashboardStrategy extends ReactiveElement {
} satisfies HomeMainViewStrategyConfig,
},
...areaViews,
lightView,
climateView,
securityView,
mediaPlayersView,

View File

@@ -1,29 +1,22 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import {
findEntities,
generateEntityFilter,
type EntityFilter,
} from "../../../common/entity/entity_filter";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import {
computeAreaTileCardConfig,
getAreas,
getFloors,
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
} from "../areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "./helpers/home-structure";
import { findEntities, HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
export interface LightsViewStrategyConfig {
type: "lights";
export interface HomeLightsViewStrategyConfig {
type: "home-lights";
}
export const lightEntityFilters: EntityFilter[] = [
{ domain: "light", entity_category: "none" },
];
const processAreasForLights = (
areaIds: string[],
hass: HomeAssistant,
@@ -52,6 +45,10 @@ const processAreasForLights = (
heading_style: "subtitle",
type: "heading",
heading: area.name,
tap_action: {
action: "navigate",
navigation_path: `areas-${area.area_id}`,
},
});
cards.push(...areaCards);
}
@@ -60,10 +57,10 @@ const processAreasForLights = (
return cards;
};
@customElement("lights-view-strategy")
export class LightsViewStrategy extends ReactiveElement {
@customElement("home-lights-view-strategy")
export class HomeLightsViewStrategy extends ReactiveElement {
static async generate(
_config: LightsViewStrategyConfig,
_config: HomeLightsViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const areas = getAreas(hass.areas);
@@ -74,7 +71,7 @@ export class LightsViewStrategy extends ReactiveElement {
const allEntities = Object.keys(hass.states);
const lightsFilters = lightEntityFilters.map((filter) =>
const lightsFilters = HOME_SUMMARIES_FILTERS.lights.map((filter) =>
generateEntityFilter(hass, filter)
);
@@ -144,6 +141,6 @@ export class LightsViewStrategy extends ReactiveElement {
declare global {
interface HTMLElementTagNameMap {
"lights-view-strategy": LightsViewStrategy;
"home-lights-view-strategy": HomeLightsViewStrategy;
}
}

View File

@@ -131,7 +131,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/lights?historyBack=1",
navigation_path: "lights",
},
grid_options: {
rows: 2,
@@ -144,7 +144,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/climate?historyBack=1",
navigation_path: "climate",
},
grid_options: {
rows: 2,
@@ -157,7 +157,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/security?historyBack=1",
navigation_path: "security",
},
grid_options: {
rows: 2,

View File

@@ -1,9 +1,6 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import {
findEntities,
generateEntityFilter,
} from "../../../../common/entity/entity_filter";
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
@@ -11,7 +8,7 @@ import type { HomeAssistant } from "../../../../types";
import type { MediaControlCardConfig } from "../../cards/types";
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "./helpers/home-structure";
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
import { findEntities, HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
export interface HomeMediaPlayersViewStrategyConfig {
type: "home-media-players";

View File

@@ -1,9 +1,6 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import {
findEntities,
generateEntityFilter,
} from "../../../../common/entity/entity_filter";
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
@@ -14,7 +11,7 @@ import {
getFloors,
} from "../areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "./helpers/home-structure";
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
import { findEntities, HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
export interface HomeSecurityViewStrategyConfig {
type: "home-security";

View File

@@ -323,9 +323,6 @@ export const getMyRedirects = (): Redirects => ({
category: "string?",
},
},
lights: {
redirect: "/lights",
},
});
const getRedirect = (path: string): Redirect | undefined =>

View File

@@ -1,200 +0,0 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { goBack } from "../../common/navigate";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
const SECURITY_LOVELACE_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "security",
},
},
],
};
@customElement("ha-panel-security")
class PanelSecurity extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() private _viewIndex = 0;
@state() private _lovelace?: Lovelace;
@state() private _searchParms = new URLSearchParams(window.location.search);
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
}
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass?.locale !== this.hass.locale) {
this._setLovelace();
}
}
private _back(ev) {
ev.stopPropagation();
goBack();
}
protected render(): TemplateResult {
return html`
<div class="header">
<div class="toolbar">
${this._searchParms.has("historyBack")
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
${!this.narrow
? html`<div class="main-title">
${this.hass.localize("panel.security")}
</div>`
: nothing}
</div>
</div>
<hui-view-container .hass=${this.hass}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view>
</hui-view-container>
`;
}
private _setLovelace() {
this._lovelace = {
config: SECURITY_LOVELACE_CONFIG,
rawConfig: SECURITY_LOVELACE_CONFIG,
editMode: false,
urlPath: "security",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
.header {
background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
position: fixed;
top: 0;
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-right,
0px
)
);
padding-top: var(--safe-area-inset-top);
z-index: 4;
transition: box-shadow 200ms linear;
display: flex;
flex-direction: row;
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
backdrop-filter: var(--app-header-backdrop-filter, none);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
}
:host([narrow]) .header {
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-left,
0px
) - var(--safe-area-inset-right, 0px)
);
padding-left: var(--safe-area-inset-left);
}
:host([scrolled]) .header {
box-shadow: var(
--mdc-top-app-bar-fixed-box-shadow,
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
);
}
.toolbar {
height: var(--header-height);
display: flex;
flex: 1;
align-items: center;
font-size: var(--ha-font-size-xl);
padding: 0px 12px;
font-weight: var(--ha-font-weight-normal);
box-sizing: border-box;
}
:host([narrow]) .toolbar {
padding: 0 4px;
}
.main-title {
margin: var(--margin-title);
line-height: var(--ha-line-height-normal);
flex-grow: 1;
}
hui-view-container {
position: relative;
display: flex;
min-height: 100vh;
box-sizing: border-box;
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
padding-right: var(--safe-area-inset-right);
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
}
:host([narrow]) hui-view-container {
padding-left: var(--safe-area-inset-left);
padding-inline-start: var(--safe-area-inset-left);
}
hui-view {
flex: 1 1 100%;
max-width: 100%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-panel-security": PanelSecurity;
}
}

View File

@@ -1,191 +0,0 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import {
findEntities,
generateEntityFilter,
type EntityFilter,
} from "../../../common/entity/entity_filter";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import {
computeAreaTileCardConfig,
getAreas,
getFloors,
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
export interface SecurityViewStrategyConfig {
type: "security";
}
export const securityEntityFilters: EntityFilter[] = [
{
domain: "camera",
entity_category: "none",
},
{
domain: "alarm_control_panel",
entity_category: "none",
},
{
domain: "lock",
entity_category: "none",
},
{
domain: "cover",
device_class: ["door", "garage", "gate"],
entity_category: "none",
},
{
domain: "binary_sensor",
device_class: [
// Locks
"lock",
// Openings
"door",
"window",
"garage_door",
"opening",
// Safety
"carbon_monoxide",
"gas",
"moisture",
"safety",
"smoke",
"tamper",
],
entity_category: "none",
},
// We also want the tamper sensors when they are diagnostic
{
domain: "binary_sensor",
device_class: ["tamper"],
entity_category: "diagnostic",
},
];
const processAreasForSecurity = (
areaIds: string[],
hass: HomeAssistant,
entities: string[]
): LovelaceCardConfig[] => {
const cards: LovelaceCardConfig[] = [];
for (const areaId of areaIds) {
const area = hass.areas[areaId];
if (!area) continue;
const areaFilter = generateEntityFilter(hass, {
area: area.area_id,
});
const areaSecurityEntities = entities.filter(areaFilter);
const areaCards: LovelaceCardConfig[] = [];
const computeTileCard = computeAreaTileCardConfig(hass, "", false);
for (const entityId of areaSecurityEntities) {
areaCards.push(computeTileCard(entityId));
}
if (areaCards.length > 0) {
cards.push({
heading_style: "subtitle",
type: "heading",
heading: area.name,
});
cards.push(...areaCards);
}
}
return cards;
};
@customElement("security-view-strategy")
export class SecurityViewStrategy extends ReactiveElement {
static async generate(
_config: SecurityViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const areas = getAreas(hass.areas);
const floors = getFloors(hass.floors);
const home = getHomeStructure(floors, areas);
const sections: LovelaceSectionRawConfig[] = [];
const allEntities = Object.keys(hass.states);
const securityFilters = securityEntityFilters.map((filter) =>
generateEntityFilter(hass, filter)
);
const entities = findEntities(allEntities, securityFilters);
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
// Process floors
for (const floorStructure of home.floors) {
const floorId = floorStructure.id;
const areaIds = floorStructure.areas;
const floor = hass.floors[floorId];
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
floorCount > 1
? floor.name
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
},
],
};
const areaCards = processAreasForSecurity(areaIds, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
sections.push(section);
}
}
// Process unassigned areas
if (home.areas.length > 0) {
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
floorCount > 1
? hass.localize("ui.panel.lovelace.strategy.home.other_areas")
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
},
],
};
const areaCards = processAreasForSecurity(home.areas, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
sections.push(section);
}
}
return {
type: "sections",
max_columns: 2,
sections: sections || [],
};
}
}
declare global {
interface HTMLElementTagNameMap {
"security-view-strategy": SecurityViewStrategy;
}
}

View File

@@ -10,10 +10,7 @@
"todo": "To-do lists",
"developer_tools": "Developer tools",
"media_browser": "Media",
"profile": "Profile",
"lights": "Lights",
"security": "Security",
"climate": "Climate"
"profile": "Profile"
},
"state": {
"default": {
@@ -6887,6 +6884,7 @@
"home": {
"summary_list": {
"climate": "Climate",
"lights": "Lights",
"security": "Security",
"media_players": "Media players"
},