1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-18 07:56:44 +01:00
Files
frontend/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts
2026-03-27 22:08:50 +01:00

250 lines
7.5 KiB
TypeScript

import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeStateName } from "../../../../../common/entity/compute_state_name";
import { stringCompare } from "../../../../../common/string/compare";
import "../../../../../components/entity/state-badge";
import "../../../../../components/ha-area-picker";
import "../../../../../components/ha-card";
import "../../../../../components/ha-textfield";
import { updateDeviceRegistryEntry } from "../../../../../data/device/device_registry";
import type { EntityRegistryEntry } from "../../../../../data/entity/entity_registry";
import {
getAutomaticEntityIds,
subscribeEntityRegistry,
updateEntityRegistryEntry,
} from "../../../../../data/entity/entity_registry";
import type { ZHADevice } from "../../../../../data/zha";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import type { EntityRegistryStateEntry } from "../../../devices/ha-config-device-page";
@customElement("zha-device-card")
class ZHADeviceCard extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public device?: ZHADevice;
@property({ type: Boolean }) public narrow = false;
@state() private _entities: EntityRegistryEntry[] = [];
private _deviceEntities = memoizeOne(
(
deviceId: string,
entities: EntityRegistryEntry[]
): EntityRegistryStateEntry[] =>
entities
.filter((entity) => entity.device_id === deviceId)
.map((entity) => ({
...entity,
stateName: this._computeEntityName(entity),
}))
.sort((ent1, ent2) =>
stringCompare(
ent1.stateName || `zzz${ent1.entity_id}`,
ent2.stateName || `zzz${ent2.entity_id}`,
this.hass.locale.language
)
)
);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection, (entities) => {
this._entities = entities;
}),
];
}
protected render() {
if (!this.hass || !this.device) {
return nothing;
}
const entities = this._deviceEntities(
this.device.device_reg_id,
this._entities
);
return html`
<ha-card>
<div class="card-content">
<div>
<div class="model">${this.device.model}</div>
<div class="manuf">
${this.hass.localize("ui.dialogs.zha_device_info.manuf", {
manufacturer: this.device.manufacturer,
})}
</div>
</div>
<div class="device-entities">
${entities.map((entity) =>
!entity.disabled_by
? html`
<state-badge
@click=${this._openMoreInfo}
.title=${entity.stateName!}
.hass=${this.hass}
.stateObj=${this.hass!.states[entity.entity_id]}
slot="item-icon"
></state-badge>
`
: ""
)}
</div>
<ha-textfield
type="string"
@change=${this._rename}
.value=${this.device.user_given_name || this.device.name}
.label=${this.hass.localize(
"ui.dialogs.zha_device_info.zha_device_card.device_name_placeholder"
)}
></ha-textfield>
<ha-area-picker
.hass=${this.hass}
.device=${this.device.device_reg_id}
@value-changed=${this._areaPicked}
></ha-area-picker>
</div>
</ha-card>
`;
}
private async _rename(event): Promise<void> {
if (!this.hass || !this.device) {
return;
}
const device = this.device;
const oldDeviceName = device.user_given_name || device.name;
const newDeviceName = event.target.value;
this.device.user_given_name = newDeviceName;
await updateDeviceRegistryEntry(this.hass, device.device_reg_id, {
name_by_user: newDeviceName,
});
if (!oldDeviceName || !newDeviceName || oldDeviceName === newDeviceName) {
return;
}
const entities = this._deviceEntities(device.device_reg_id, this._entities);
const entityIdsMapping = await getAutomaticEntityIds(
this.hass,
entities.map((entity) => entity.entity_id)
);
const updateProms = entities.map((entity) => {
const name = entity.name;
const newEntityId = entityIdsMapping[entity.entity_id];
let newName: string | null | undefined;
if (entity.has_entity_name && !entity.name) {
newName = undefined;
} else if (
entity.has_entity_name &&
(entity.name === oldDeviceName || entity.name === newDeviceName)
) {
// clear name if it matches the device name and it uses the device name (entity naming)
newName = null;
} else if (name && name.includes(oldDeviceName)) {
newName = name.replace(oldDeviceName, newDeviceName);
}
if (newName !== undefined && !newEntityId) {
return undefined;
}
return updateEntityRegistryEntry(this.hass!, entity.entity_id, {
name: newName,
new_entity_id: newEntityId || undefined,
});
});
await Promise.all(updateProms);
}
private _openMoreInfo(ev: MouseEvent): void {
fireEvent(this, "hass-more-info", {
entityId: (ev.currentTarget as any).stateObj.entity_id,
});
}
private _computeEntityName(entity: EntityRegistryEntry): string | null {
if (this.hass.states[entity.entity_id]) {
return computeStateName(this.hass.states[entity.entity_id]);
}
return entity.name;
}
private async _areaPicked(ev: CustomEvent) {
const picker = ev.currentTarget as any;
const area = ev.detail.value;
try {
await updateDeviceRegistryEntry(this.hass, this.device!.device_reg_id, {
area_id: area,
});
this.device!.area_id = area;
} catch (err: any) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.error_saving_device",
{ error: err.message }
),
});
picker.value = null;
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.device-entities {
display: flex;
flex-wrap: wrap;
padding: 4px;
justify-content: left;
min-height: 48px;
}
.device {
width: 30%;
}
.device .name {
font-weight: var(--ha-font-weight-bold);
}
.device .manuf {
color: var(--secondary-text-color);
margin-bottom: 20px;
word-wrap: break-word;
}
.extra-info {
margin-top: 8px;
word-wrap: break-word;
}
state-badge {
cursor: pointer;
}
ha-card {
border: none;
}
ha-textfield {
width: 100%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"zha-device-card": ZHADeviceCard;
}
}