diff --git a/src/layouts/partial-panel-resolver.ts b/src/layouts/partial-panel-resolver.ts
index 5dd07bd094..e573f456eb 100644
--- a/src/layouts/partial-panel-resolver.ts
+++ b/src/layouts/partial-panel-resolver.ts
@@ -33,6 +33,8 @@ const COMPONENTS = {
"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")
diff --git a/src/panels/climate/ha-panel-climate.ts b/src/panels/climate/ha-panel-climate.ts
new file mode 100644
index 0000000000..d2e7684785
--- /dev/null
+++ b/src/panels/climate/ha-panel-climate.ts
@@ -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 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 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`
+
+
+
+
+
+ `;
+ }
+
+ 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,
+ };
+ }
+
+ 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;
+ }
+}
diff --git a/src/panels/climate/strategies/climate-view-strategy.ts b/src/panels/climate/strategies/climate-view-strategy.ts
new file mode 100644
index 0000000000..6d4ef66a32
--- /dev/null
+++ b/src/panels/climate/strategies/climate-view-strategy.ts
@@ -0,0 +1,189 @@
+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" },
+ { domain: "humidifier" },
+ { domain: "fan" },
+ { domain: "binary_sensor", device_class: "heat" },
+ { domain: "binary_sensor", device_class: "cold" },
+ { domain: "sensor", device_class: "temperature" },
+ { domain: "sensor", device_class: "humidity" },
+ { domain: "sensor", device_class: "atmospheric_pressure" },
+];
+
+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 {
+ 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;
+ }
+}
diff --git a/src/panels/lovelace/strategies/get-strategy.ts b/src/panels/lovelace/strategies/get-strategy.ts
index 4b968febd9..09a569a628 100644
--- a/src/panels/lovelace/strategies/get-strategy.ts
+++ b/src/panels/lovelace/strategies/get-strategy.ts
@@ -51,6 +51,8 @@ const STRATEGIES: Record> = {
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": () =>
diff --git a/src/panels/security/ha-panel-security.ts b/src/panels/security/ha-panel-security.ts
new file mode 100644
index 0000000000..f44630659a
--- /dev/null
+++ b/src/panels/security/ha-panel-security.ts
@@ -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`
+
+
+
+
+
+ `;
+ }
+
+ 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;
+ }
+}
diff --git a/src/panels/security/strategies/security-view-strategy.ts b/src/panels/security/strategies/security-view-strategy.ts
new file mode 100644
index 0000000000..a0b9579093
--- /dev/null
+++ b/src/panels/security/strategies/security-view-strategy.ts
@@ -0,0 +1,164 @@
+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: "binary_sensor", device_class: "door" },
+ { domain: "binary_sensor", device_class: "garage_door" },
+ { domain: "binary_sensor", device_class: "lock" },
+ { domain: "binary_sensor", device_class: "opening" },
+ { domain: "binary_sensor", device_class: "window" },
+ { domain: "binary_sensor", device_class: "motion" },
+ { domain: "binary_sensor", device_class: "occupancy" },
+ { domain: "binary_sensor", device_class: "presence" },
+ { domain: "binary_sensor", device_class: "safety" },
+ { domain: "binary_sensor", device_class: "smoke" },
+ { domain: "binary_sensor", device_class: "gas" },
+ { domain: "binary_sensor", device_class: "problem" },
+ { domain: "binary_sensor", device_class: "tamper" },
+ { domain: "lock" },
+ { domain: "alarm_control_panel" },
+ { domain: "camera" },
+];
+
+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 {
+ 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;
+ }
+}
diff --git a/src/translations/en.json b/src/translations/en.json
index ada639a17a..3de5e8114f 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -11,7 +11,9 @@
"developer_tools": "Developer tools",
"media_browser": "Media",
"profile": "Profile",
- "lights": "Lights"
+ "lights": "Lights",
+ "security": "Security",
+ "climate": "Climate"
},
"state": {
"default": {