mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-11 20:10:24 +00:00
Compare commits
6 Commits
copilot/fi
...
lights-pan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89a0fa61c8 | ||
|
|
b10ca8ac23 | ||
|
|
5fe4656a86 | ||
|
|
dba42bb7b5 | ||
|
|
5aac3eb2d5 | ||
|
|
93cf4c0404 |
@@ -122,3 +122,22 @@ export const generateEntityFilter = (
|
|||||||
return true;
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ export class HaIconOverflowMenu extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public narrow = false;
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult | typeof nothing {
|
||||||
|
if (this.items.length === 0) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
return html`
|
return html`
|
||||||
${this.narrow
|
${this.narrow
|
||||||
? html` <!-- Collapsed representation for small screens -->
|
? html` <!-- Collapsed representation for small screens -->
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ const COMPONENTS = {
|
|||||||
todo: () => import("../panels/todo/ha-panel-todo"),
|
todo: () => import("../panels/todo/ha-panel-todo"),
|
||||||
"media-browser": () =>
|
"media-browser": () =>
|
||||||
import("../panels/media-browser/ha-panel-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")
|
@customElement("partial-panel-resolver")
|
||||||
|
|||||||
214
src/panels/climate/ha-panel-climate.ts
Normal file
214
src/panels/climate/ha-panel-climate.ts
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
203
src/panels/climate/strategies/climate-view-strategy.ts
Normal file
203
src/panels/climate/strategies/climate-view-strategy.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/panels/config/climate/ha-config-climate.ts
Normal file
42
src/panels/config/climate/ha-config-climate.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -577,6 +577,10 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
|
|||||||
load: () =>
|
load: () =>
|
||||||
import("./application_credentials/ha-config-application-credentials"),
|
import("./application_credentials/ha-config-application-credentials"),
|
||||||
},
|
},
|
||||||
|
climate: {
|
||||||
|
tag: "ha-config-climate",
|
||||||
|
load: () => import("./climate/ha-config-climate"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -238,13 +238,17 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
narrow
|
narrow
|
||||||
.items=${[
|
.items=${[
|
||||||
{
|
...(this._canEdit(dashboard.url_path)
|
||||||
path: mdiPencil,
|
? [
|
||||||
label: this.hass.localize(
|
{
|
||||||
"ui.panel.config.lovelace.dashboards.picker.edit"
|
path: mdiPencil,
|
||||||
),
|
label: this.hass.localize(
|
||||||
action: () => this._handleEdit(dashboard),
|
"ui.panel.config.lovelace.dashboards.picker.edit"
|
||||||
},
|
),
|
||||||
|
action: () => this._handleEdit(dashboard),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
...(this._canDelete(dashboard.url_path)
|
...(this._canDelete(dashboard.url_path)
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -294,7 +298,49 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
mode: "storage",
|
mode: "storage",
|
||||||
url_path: "energy",
|
url_path: "energy",
|
||||||
filename: "",
|
filename: "",
|
||||||
iconColor: "var(--label-badge-yellow)",
|
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)",
|
||||||
default: false,
|
default: false,
|
||||||
require_admin: false,
|
require_admin: false,
|
||||||
});
|
});
|
||||||
@@ -392,12 +438,28 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
navigate("/config/energy");
|
navigate("/config/energy");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (urlPath === "climate") {
|
||||||
|
navigate("/config/climate");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const dashboard = this._dashboards.find((res) => res.url_path === urlPath);
|
const dashboard = this._dashboards.find((res) => res.url_path === urlPath);
|
||||||
this._openDetailDialog(dashboard, urlPath);
|
this._openDetailDialog(dashboard, urlPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _canDelete(urlPath: string) {
|
private _canDelete(urlPath: string) {
|
||||||
if (urlPath === "lovelace" || urlPath === "energy") {
|
if (
|
||||||
|
urlPath === "lovelace" ||
|
||||||
|
urlPath === "energy" ||
|
||||||
|
urlPath === "lights" ||
|
||||||
|
urlPath === "security"
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _canEdit(urlPath: string) {
|
||||||
|
if (urlPath === "lights" || urlPath === "security") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
200
src/panels/lights/ha-panel-lights.ts
Normal file
200
src/panels/lights/ha-panel-lights.ts
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,29 @@
|
|||||||
import { ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
|
import {
|
||||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
findEntities,
|
||||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
generateEntityFilter,
|
||||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
type EntityFilter,
|
||||||
import type { HomeAssistant } from "../../../../types";
|
} 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 {
|
import {
|
||||||
computeAreaTileCardConfig,
|
computeAreaTileCardConfig,
|
||||||
getAreas,
|
getAreas,
|
||||||
getFloors,
|
getFloors,
|
||||||
} from "../areas/helpers/areas-strategy-helper";
|
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||||
import { getHomeStructure } from "./helpers/home-structure";
|
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
|
||||||
import { findEntities, HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
|
||||||
|
|
||||||
export interface HomeLightsViewStrategyConfig {
|
export interface LightsViewStrategyConfig {
|
||||||
type: "home-lights";
|
type: "lights";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const lightEntityFilters: EntityFilter[] = [
|
||||||
|
{ domain: "light", entity_category: "none" },
|
||||||
|
];
|
||||||
|
|
||||||
const processAreasForLights = (
|
const processAreasForLights = (
|
||||||
areaIds: string[],
|
areaIds: string[],
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -45,10 +52,6 @@ const processAreasForLights = (
|
|||||||
heading_style: "subtitle",
|
heading_style: "subtitle",
|
||||||
type: "heading",
|
type: "heading",
|
||||||
heading: area.name,
|
heading: area.name,
|
||||||
tap_action: {
|
|
||||||
action: "navigate",
|
|
||||||
navigation_path: `areas-${area.area_id}`,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
cards.push(...areaCards);
|
cards.push(...areaCards);
|
||||||
}
|
}
|
||||||
@@ -57,10 +60,10 @@ const processAreasForLights = (
|
|||||||
return cards;
|
return cards;
|
||||||
};
|
};
|
||||||
|
|
||||||
@customElement("home-lights-view-strategy")
|
@customElement("lights-view-strategy")
|
||||||
export class HomeLightsViewStrategy extends ReactiveElement {
|
export class LightsViewStrategy extends ReactiveElement {
|
||||||
static async generate(
|
static async generate(
|
||||||
_config: HomeLightsViewStrategyConfig,
|
_config: LightsViewStrategyConfig,
|
||||||
hass: HomeAssistant
|
hass: HomeAssistant
|
||||||
): Promise<LovelaceViewConfig> {
|
): Promise<LovelaceViewConfig> {
|
||||||
const areas = getAreas(hass.areas);
|
const areas = getAreas(hass.areas);
|
||||||
@@ -71,7 +74,7 @@ export class HomeLightsViewStrategy extends ReactiveElement {
|
|||||||
|
|
||||||
const allEntities = Object.keys(hass.states);
|
const allEntities = Object.keys(hass.states);
|
||||||
|
|
||||||
const lightsFilters = HOME_SUMMARIES_FILTERS.lights.map((filter) =>
|
const lightsFilters = lightEntityFilters.map((filter) =>
|
||||||
generateEntityFilter(hass, filter)
|
generateEntityFilter(hass, filter)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -141,6 +144,6 @@ export class HomeLightsViewStrategy extends ReactiveElement {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"home-lights-view-strategy": HomeLightsViewStrategy;
|
"lights-view-strategy": LightsViewStrategy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,10 @@ import { ifDefined } from "lit/directives/if-defined";
|
|||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import { computeCssColor } from "../../../common/color/compute-color";
|
import { computeCssColor } from "../../../common/color/compute-color";
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
import { generateEntityFilter } from "../../../common/entity/entity_filter";
|
import {
|
||||||
|
findEntities,
|
||||||
|
generateEntityFilter,
|
||||||
|
} from "../../../common/entity/entity_filter";
|
||||||
import { formatNumber } from "../../../common/number/format_number";
|
import { formatNumber } from "../../../common/number/format_number";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-icon";
|
import "../../../components/ha-icon";
|
||||||
@@ -19,7 +22,6 @@ import { actionHandler } from "../common/directives/action-handler-directive";
|
|||||||
import { handleAction } from "../common/handle-action";
|
import { handleAction } from "../common/handle-action";
|
||||||
import { hasAction } from "../common/has-action";
|
import { hasAction } from "../common/has-action";
|
||||||
import {
|
import {
|
||||||
findEntities,
|
|
||||||
getSummaryLabel,
|
getSummaryLabel,
|
||||||
HOME_SUMMARIES_FILTERS,
|
HOME_SUMMARIES_FILTERS,
|
||||||
HOME_SUMMARIES_ICONS,
|
HOME_SUMMARIES_ICONS,
|
||||||
|
|||||||
@@ -44,12 +44,15 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
|
|||||||
area: () => import("./areas/area-view-strategy"),
|
area: () => import("./areas/area-view-strategy"),
|
||||||
"areas-overview": () => import("./areas/areas-overview-view-strategy"),
|
"areas-overview": () => import("./areas/areas-overview-view-strategy"),
|
||||||
"home-main": () => import("./home/home-main-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-climate": () => import("./home/home-climate-view-strategy"),
|
||||||
"home-security": () => import("./home/home-security-view-strategy"),
|
"home-security": () => import("./home/home-security-view-strategy"),
|
||||||
"home-media-players": () =>
|
"home-media-players": () =>
|
||||||
import("./home/home-media-players-view-strategy"),
|
import("./home/home-media-players-view-strategy"),
|
||||||
"home-area": () => import("./home/home-area-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: {
|
section: {
|
||||||
"common-controls": () =>
|
"common-controls": () =>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ interface HomeStructure {
|
|||||||
id: string;
|
id: string;
|
||||||
areas: string[];
|
areas: string[];
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
areas: string[];
|
areas: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type {
|
import type { EntityFilter } from "../../../../../common/entity/entity_filter";
|
||||||
EntityFilter,
|
|
||||||
EntityFilterFunc,
|
|
||||||
} from "../../../../../common/entity/entity_filter";
|
|
||||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
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 = [
|
export const HOME_SUMMARIES = [
|
||||||
"lights",
|
"lights",
|
||||||
@@ -21,97 +21,18 @@ export const HOME_SUMMARIES_ICONS: Record<HomeSummary, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
|
export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
|
||||||
lights: [{ domain: "light", entity_category: "none" }],
|
lights: lightEntityFilters,
|
||||||
climate: [
|
climate: climateEntityFilters,
|
||||||
{ domain: "climate", entity_category: "none" },
|
security: securityEntityFilters,
|
||||||
{ 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" }],
|
media_players: [{ domain: "media_player", entity_category: "none" }],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const findEntities = (
|
export const getSummaryLabel = (
|
||||||
entities: string[],
|
localize: LocalizeFunc,
|
||||||
filters: EntityFilterFunc[]
|
summary: HomeSummary
|
||||||
): string[] => {
|
) => {
|
||||||
const seen = new Set<string>();
|
if (summary === "lights" || summary === "climate" || summary === "security") {
|
||||||
const results: string[] = [];
|
return localize(`panel.${summary}`);
|
||||||
|
|
||||||
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}`);
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { ReactiveElement } from "lit";
|
|||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||||
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
|
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
|
||||||
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
|
import {
|
||||||
|
findEntities,
|
||||||
|
generateEntityFilter,
|
||||||
|
} from "../../../../common/entity/entity_filter";
|
||||||
import { clamp } from "../../../../common/number/clamp";
|
import { clamp } from "../../../../common/number/clamp";
|
||||||
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||||
@@ -12,7 +15,6 @@ import type { HomeAssistant } from "../../../../types";
|
|||||||
import type { HeadingCardConfig } from "../../cards/types";
|
import type { HeadingCardConfig } from "../../cards/types";
|
||||||
import { computeAreaTileCardConfig } from "../areas/helpers/areas-strategy-helper";
|
import { computeAreaTileCardConfig } from "../areas/helpers/areas-strategy-helper";
|
||||||
import {
|
import {
|
||||||
findEntities,
|
|
||||||
getSummaryLabel,
|
getSummaryLabel,
|
||||||
HOME_SUMMARIES,
|
HOME_SUMMARIES,
|
||||||
HOME_SUMMARIES_FILTERS,
|
HOME_SUMMARIES_FILTERS,
|
||||||
@@ -113,7 +115,7 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
|||||||
computeHeadingCard(
|
computeHeadingCard(
|
||||||
getSummaryLabel(hass.localize, "lights"),
|
getSummaryLabel(hass.localize, "lights"),
|
||||||
HOME_SUMMARIES_ICONS.lights,
|
HOME_SUMMARIES_ICONS.lights,
|
||||||
"lights"
|
"/lights?historyBack=1"
|
||||||
),
|
),
|
||||||
...lights.map(computeTileCard),
|
...lights.map(computeTileCard),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
|
import {
|
||||||
|
findEntities,
|
||||||
|
generateEntityFilter,
|
||||||
|
} from "../../../../common/entity/entity_filter";
|
||||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||||
@@ -11,7 +14,7 @@ import {
|
|||||||
getFloors,
|
getFloors,
|
||||||
} from "../areas/helpers/areas-strategy-helper";
|
} from "../areas/helpers/areas-strategy-helper";
|
||||||
import { getHomeStructure } from "./helpers/home-structure";
|
import { getHomeStructure } from "./helpers/home-structure";
|
||||||
import { findEntities, HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
||||||
|
|
||||||
export interface HomeClimateViewStrategyConfig {
|
export interface HomeClimateViewStrategyConfig {
|
||||||
type: "home-climate";
|
type: "home-climate";
|
||||||
|
|||||||
@@ -62,16 +62,6 @@ 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 = {
|
const climateView = {
|
||||||
title: getSummaryLabel(hass.localize, "climate"),
|
title: getSummaryLabel(hass.localize, "climate"),
|
||||||
path: "climate",
|
path: "climate",
|
||||||
@@ -113,7 +103,6 @@ export class HomeDashboardStrategy extends ReactiveElement {
|
|||||||
} satisfies HomeMainViewStrategyConfig,
|
} satisfies HomeMainViewStrategyConfig,
|
||||||
},
|
},
|
||||||
...areaViews,
|
...areaViews,
|
||||||
lightView,
|
|
||||||
climateView,
|
climateView,
|
||||||
securityView,
|
securityView,
|
||||||
mediaPlayersView,
|
mediaPlayersView,
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
|||||||
vertical: true,
|
vertical: true,
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "navigate",
|
action: "navigate",
|
||||||
navigation_path: "lights",
|
navigation_path: "/lights?historyBack=1",
|
||||||
},
|
},
|
||||||
grid_options: {
|
grid_options: {
|
||||||
rows: 2,
|
rows: 2,
|
||||||
@@ -144,7 +144,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
|||||||
vertical: true,
|
vertical: true,
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "navigate",
|
action: "navigate",
|
||||||
navigation_path: "climate",
|
navigation_path: "/climate?historyBack=1",
|
||||||
},
|
},
|
||||||
grid_options: {
|
grid_options: {
|
||||||
rows: 2,
|
rows: 2,
|
||||||
@@ -157,7 +157,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
|||||||
vertical: true,
|
vertical: true,
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "navigate",
|
action: "navigate",
|
||||||
navigation_path: "security",
|
navigation_path: "/security?historyBack=1",
|
||||||
},
|
},
|
||||||
grid_options: {
|
grid_options: {
|
||||||
rows: 2,
|
rows: 2,
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
|
import {
|
||||||
|
findEntities,
|
||||||
|
generateEntityFilter,
|
||||||
|
} from "../../../../common/entity/entity_filter";
|
||||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||||
@@ -8,7 +11,7 @@ import type { HomeAssistant } from "../../../../types";
|
|||||||
import type { MediaControlCardConfig } from "../../cards/types";
|
import type { MediaControlCardConfig } from "../../cards/types";
|
||||||
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper";
|
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper";
|
||||||
import { getHomeStructure } from "./helpers/home-structure";
|
import { getHomeStructure } from "./helpers/home-structure";
|
||||||
import { findEntities, HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
||||||
|
|
||||||
export interface HomeMediaPlayersViewStrategyConfig {
|
export interface HomeMediaPlayersViewStrategyConfig {
|
||||||
type: "home-media-players";
|
type: "home-media-players";
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
|
import {
|
||||||
|
findEntities,
|
||||||
|
generateEntityFilter,
|
||||||
|
} from "../../../../common/entity/entity_filter";
|
||||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||||
@@ -11,7 +14,7 @@ import {
|
|||||||
getFloors,
|
getFloors,
|
||||||
} from "../areas/helpers/areas-strategy-helper";
|
} from "../areas/helpers/areas-strategy-helper";
|
||||||
import { getHomeStructure } from "./helpers/home-structure";
|
import { getHomeStructure } from "./helpers/home-structure";
|
||||||
import { findEntities, HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
||||||
|
|
||||||
export interface HomeSecurityViewStrategyConfig {
|
export interface HomeSecurityViewStrategyConfig {
|
||||||
type: "home-security";
|
type: "home-security";
|
||||||
|
|||||||
@@ -323,6 +323,9 @@ export const getMyRedirects = (): Redirects => ({
|
|||||||
category: "string?",
|
category: "string?",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
lights: {
|
||||||
|
redirect: "/lights",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const getRedirect = (path: string): Redirect | undefined =>
|
const getRedirect = (path: string): Redirect | undefined =>
|
||||||
|
|||||||
200
src/panels/security/ha-panel-security.ts
Normal file
200
src/panels/security/ha-panel-security.ts
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
191
src/panels/security/strategies/security-view-strategy.ts
Normal file
191
src/panels/security/strategies/security-view-strategy.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,10 @@
|
|||||||
"todo": "To-do lists",
|
"todo": "To-do lists",
|
||||||
"developer_tools": "Developer tools",
|
"developer_tools": "Developer tools",
|
||||||
"media_browser": "Media",
|
"media_browser": "Media",
|
||||||
"profile": "Profile"
|
"profile": "Profile",
|
||||||
|
"lights": "Lights",
|
||||||
|
"security": "Security",
|
||||||
|
"climate": "Climate"
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"default": {
|
"default": {
|
||||||
@@ -6884,7 +6887,6 @@
|
|||||||
"home": {
|
"home": {
|
||||||
"summary_list": {
|
"summary_list": {
|
||||||
"climate": "Climate",
|
"climate": "Climate",
|
||||||
"lights": "Lights",
|
|
||||||
"security": "Security",
|
"security": "Security",
|
||||||
"media_players": "Media players"
|
"media_players": "Media players"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user