diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts
index b79abb66a4..4a22d3775e 100644
--- a/src/components/ha-area-picker.ts
+++ b/src/components/ha-area-picker.ts
@@ -340,7 +340,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
item-value-path="area_id"
item-id-path="area_id"
item-label-path="name"
- .value=${this._value}
+ .value=${this.value}
.disabled=${this.disabled}
${comboBoxRenderer(rowRenderer)}
@opened-changed=${this._openedChanged}
@@ -431,12 +431,24 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
name,
});
this._areas = [...this._areas!, area];
+ (this.comboBox as any).items = this._getAreas(
+ this._areas!,
+ this._devices!,
+ this._entities!,
+ this.includeDomains,
+ this.excludeDomains,
+ this.includeDeviceClasses,
+ this.deviceFilter,
+ this.entityFilter,
+ this.noAdd
+ );
this._setValue(area.area_id);
} catch (err: any) {
showAlertDialog(this, {
- text: this.hass.localize(
+ title: this.hass.localize(
"ui.components.area-picker.add_dialog.failed_create_area"
),
+ text: err.message,
});
}
},
diff --git a/src/data/area_registry.ts b/src/data/area_registry.ts
index bb820261a9..3e432ce626 100644
--- a/src/data/area_registry.ts
+++ b/src/data/area_registry.ts
@@ -7,10 +7,12 @@ import { HomeAssistant } from "../types";
export interface AreaRegistryEntry {
area_id: string;
name: string;
+ picture?: string;
}
export interface AreaRegistryEntryMutableParams {
name: string;
+ picture?: string | null;
}
export const createAreaRegistryEntry = (
diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts
index 7072c44227..becf455fc0 100644
--- a/src/data/entity_registry.ts
+++ b/src/data/entity_registry.ts
@@ -66,7 +66,7 @@ export const computeEntityRegistryName = (
return entry.name;
}
const state = hass.states[entry.entity_id];
- return state ? computeStateName(state) : null;
+ return state ? computeStateName(state) : entry.entity_id;
};
export const getExtendedEntityRegistryEntry = (
diff --git a/src/dialogs/image-cropper-dialog/show-image-cropper-dialog.ts b/src/dialogs/image-cropper-dialog/show-image-cropper-dialog.ts
index 6ce2090070..4f7ff03d14 100644
--- a/src/dialogs/image-cropper-dialog/show-image-cropper-dialog.ts
+++ b/src/dialogs/image-cropper-dialog/show-image-cropper-dialog.ts
@@ -4,7 +4,7 @@ export interface CropOptions {
round: boolean;
type?: "image/jpeg" | "image/png";
quality?: number;
- aspectRatio: number;
+ aspectRatio?: number;
}
export interface HaImageCropperDialogParams {
diff --git a/src/panels/config/areas/dialog-area-registry-detail.ts b/src/panels/config/areas/dialog-area-registry-detail.ts
index f2fc039470..9bbb35de18 100644
--- a/src/panels/config/areas/dialog-area-registry-detail.ts
+++ b/src/panels/config/areas/dialog-area-registry-detail.ts
@@ -3,19 +3,31 @@ import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
-import { navigate } from "../../../common/navigate";
import { createCloseHeading } from "../../../components/ha-dialog";
+import "../../../components/ha-alert";
+import "../../../components/ha-picture-upload";
+import type { HaPictureUpload } from "../../../components/ha-picture-upload";
import { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
+import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail";
+const cropOptions: CropOptions = {
+ round: false,
+ type: "image/jpeg",
+ quality: 0.75,
+ aspectRatio: 1.78,
+};
+
class DialogAreaDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _name!: string;
+ @state() private _picture!: string | null;
+
@state() private _error?: string;
@state() private _params?: AreaRegistryDetailDialogParams;
@@ -28,6 +40,7 @@ class DialogAreaDetail extends LitElement {
this._params = params;
this._error = undefined;
this._name = this._params.entry ? this._params.entry.name : "";
+ this._picture = this._params.entry?.picture || null;
await this.updateComplete;
}
@@ -55,7 +68,9 @@ class DialogAreaDetail extends LitElement {
)}
>
- ${this._error ? html`
${this._error}
` : ""}
+ ${this._error
+ ? html`
${this._error} `
+ : ""}
${entry
? html`
@@ -78,6 +93,13 @@ class DialogAreaDetail extends LitElement {
)}
.invalid=${nameInvalid}
>
+
${entry
@@ -120,18 +142,24 @@ class DialogAreaDetail extends LitElement {
this._name = ev.detail.value;
}
+ private _pictureChanged(ev: PolymerChangedEvent) {
+ this._error = undefined;
+ this._picture = (ev.target as HaPictureUpload).value;
+ }
+
private async _updateEntry() {
this._submitting = true;
try {
const values: AreaRegistryEntryMutableParams = {
name: this._name.trim(),
+ picture: this._picture,
};
if (this._params!.entry) {
await this._params!.updateEntry!(values);
} else {
await this._params!.createEntry!(values);
}
- this._params = undefined;
+ this.closeDialog();
} catch (err: any) {
this._error =
err.message ||
@@ -145,13 +173,11 @@ class DialogAreaDetail extends LitElement {
this._submitting = true;
try {
if (await this._params!.removeEntry!()) {
- this._params = undefined;
+ this.closeDialog();
}
} finally {
this._submitting = false;
}
-
- navigate("/config/areas/dashboard");
}
static get styles(): CSSResultGroup {
@@ -161,9 +187,6 @@ class DialogAreaDetail extends LitElement {
.form {
padding-bottom: 24px;
}
- .error {
- color: var(--error-color);
- }
`,
];
}
diff --git a/src/panels/config/areas/ha-config-area-page.ts b/src/panels/config/areas/ha-config-area-page.ts
index ece1334bce..2de91d403b 100644
--- a/src/panels/config/areas/ha-config-area-page.ts
+++ b/src/panels/config/areas/ha-config-area-page.ts
@@ -1,13 +1,17 @@
import "@material/mwc-button";
-import { mdiCog } from "@mdi/js";
+import "@polymer/paper-item/paper-item";
+import "@polymer/paper-item/paper-item-body";
+import { mdiImagePlus, mdiPencil } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeStateName } from "../../../common/entity/compute_state_name";
+import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
+import "../../../components/ha-icon-next";
import {
AreaRegistryEntry,
deleteAreaRegistryEntry,
@@ -134,25 +138,59 @@ class HaConfigAreaPage extends LitElement {
.tabs=${configSections.integrations}
.route=${this.route}
>
- ${this.narrow ? html` ${area.name} ` : ""}
-
-
+ ${this.narrow
+ ? html` ${area.name}
+ `
+ : ""}
${!this.narrow
? html`
-
${area.name}
+
+ ${area.name}
+
+
`
: ""}
+ ${area.picture
+ ? html`
+

+
`
+ : html`
+
+ `}
${devices.length
@@ -181,7 +219,8 @@ class HaConfigAreaPage extends LitElement {
.header=${this.hass.localize(
"ui.panel.config.areas.editor.linked_entities_caption"
)}
- >${entities.length
+ >
+ ${entities.length
? entities.map(
(entity) =>
html`
@@ -390,6 +429,7 @@ class HaConfigAreaPage extends LitElement {
try {
await deleteAreaRegistryEntry(this.hass!, entry!.area_id);
+ afterNextRender(() => history.back());
return true;
} catch (err: any) {
return false;
@@ -403,7 +443,7 @@ class HaConfigAreaPage extends LitElement {
haStyle,
css`
h1 {
- margin-top: 0;
+ margin: 0;
font-family: var(--paper-font-headline_-_font-family);
-webkit-font-smoothing: var(
--paper-font-headline_-_-webkit-font-smoothing
@@ -413,6 +453,13 @@ class HaConfigAreaPage extends LitElement {
letter-spacing: var(--paper-font-headline_-_letter-spacing);
line-height: var(--paper-font-headline_-_line-height);
opacity: var(--dark-primary-opacity);
+ display: flex;
+ align-items: center;
+ }
+
+ img {
+ border-radius: var(--ha-card-border-radius, 4px);
+ width: 100%;
}
.container {
@@ -458,6 +505,34 @@ class HaConfigAreaPage extends LitElement {
paper-item.no-link {
cursor: default;
}
+
+ ha-card > a:first-child {
+ display: block;
+ }
+ ha-card > *:first-child {
+ margin-top: -16px;
+ }
+ .img-container {
+ position: relative;
+ }
+ .img-edit-btn {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ display: none;
+ }
+ .img-container:hover .img-edit-btn {
+ display: block;
+ }
+ .img-edit-btn::before {
+ content: "";
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: var(--card-background-color);
+ opacity: 0.5;
+ border-radius: 50%;
+ }
`,
];
}
diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts
index a2d27e63b7..d694234ea6 100644
--- a/src/panels/config/areas/ha-config-areas-dashboard.ts
+++ b/src/panels/config/areas/ha-config-areas-dashboard.ts
@@ -1,15 +1,8 @@
import { mdiHelpCircle, mdiPlus } from "@mdi/js";
-import "@polymer/paper-item/paper-item";
-import "@polymer/paper-item/paper-item-body";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
+import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
-import { HASSDomEvent } from "../../../common/dom/fire_event";
-import { navigate } from "../../../common/navigate";
-import {
- DataTableColumnContainer,
- RowClickedEvent,
-} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-fab";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
@@ -21,7 +14,7 @@ import type { DeviceRegistryEntry } from "../../../data/device_registry";
import type { EntityRegistryEntry } from "../../../data/entity_registry";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-loading-screen";
-import "../../../layouts/hass-tabs-subpage-data-table";
+import "../../../layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../types";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
@@ -53,97 +46,51 @@ export class HaConfigAreasDashboard extends LitElement {
entities: EntityRegistryEntry[]
) =>
areas.map((area) => {
+ let noDevicesInArea = 0;
+ let noServicesInArea = 0;
+ let noEntitiesInArea = 0;
+
const devicesInArea = new Set();
for (const device of devices) {
if (device.area_id === area.area_id) {
devicesInArea.add(device.id);
+ if (device.entry_type === "service") {
+ noServicesInArea++;
+ } else {
+ noDevicesInArea++;
+ }
}
}
- let entitiesInArea = 0;
-
for (const entity of entities) {
if (
entity.area_id
? entity.area_id === area.area_id
: devicesInArea.has(entity.device_id)
) {
- entitiesInArea++;
+ noEntitiesInArea++;
}
}
return {
...area,
- devices: devicesInArea.size,
- entities: entitiesInArea,
+ devices: noDevicesInArea,
+ services: noServicesInArea,
+ entities: noEntitiesInArea,
};
})
);
- private _columns = memoizeOne(
- (narrow: boolean): DataTableColumnContainer =>
- narrow
- ? {
- name: {
- title: this.hass.localize(
- "ui.panel.config.areas.data_table.area"
- ),
- sortable: true,
- filterable: true,
- grows: true,
- direction: "asc",
- },
- }
- : {
- name: {
- title: this.hass.localize(
- "ui.panel.config.areas.data_table.area"
- ),
- sortable: true,
- filterable: true,
- grows: true,
- direction: "asc",
- },
- devices: {
- title: this.hass.localize(
- "ui.panel.config.areas.data_table.devices"
- ),
- sortable: true,
- type: "numeric",
- width: "20%",
- direction: "asc",
- },
- entities: {
- title: this.hass.localize(
- "ui.panel.config.areas.data_table.entities"
- ),
- sortable: true,
- type: "numeric",
- width: "20%",
- direction: "asc",
- },
- }
- );
-
protected render(): TemplateResult {
return html`
-
+
-
+
`;
}
@@ -191,11 +190,6 @@ export class HaConfigAreasDashboard extends LitElement {
});
}
- private _handleRowClicked(ev: HASSDomEvent) {
- const areaId = ev.detail.id;
- navigate(`/config/areas/area/${areaId}`);
- }
-
private _openDialog(entry?: AreaRegistryEntry) {
showAreaRegistryDetailDialog(this, {
entry,
@@ -210,6 +204,51 @@ export class HaConfigAreasDashboard extends LitElement {
--app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color);
}
+ .container {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ grid-gap: 16px 16px;
+ padding: 8px 16px 16px;
+ margin: 0 auto 64px auto;
+ max-width: 1000px;
+ }
+ .container > * {
+ max-width: 500px;
+ }
+ ha-card {
+ overflow: hidden;
+ }
+ a {
+ text-decoration: none;
+ }
+ h1 {
+ padding-bottom: 0;
+ }
+ .picture {
+ height: 150px;
+ width: 100%;
+ background-size: cover;
+ background-position: center;
+ position: relative;
+ }
+ .picture.placeholder::before {
+ position: absolute;
+ content: "";
+ width: 100%;
+ height: 100%;
+ background-color: var(--sidebar-selected-icon-color);
+ opacity: 0.12;
+ }
+ .card-content {
+ min-height: 16px;
+ color: var(--secondary-text-color);
+ }
`;
}
}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-config-areas-dashboard": HaConfigAreasDashboard;
+ }
+}
diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts
index 055b770147..6f9569e6cf 100644
--- a/src/panels/config/entities/ha-config-entities.ts
+++ b/src/panels/config/entities/ha-config-entities.ts
@@ -376,9 +376,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
result.push({
...entry,
entity,
- name:
- computeEntityRegistryName(this.hass!, entry) ||
- this.hass.localize("state.default.unavailable"),
+ name: computeEntityRegistryName(this.hass!, entry),
unavailable,
restored,
area: area ? area.name : undefined,
diff --git a/src/translations/en.json b/src/translations/en.json
index c5df32dc8a..9a565b0875 100755
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -930,6 +930,7 @@
"caption": "Areas",
"description": "Group devices and entities into areas",
"edit_settings": "Area settings",
+ "add_picture": "Add a picture",
"data_table": {
"area": "Area",
"devices": "Devices",