Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
415 changes: 415 additions & 0 deletions src/data/matter-lock.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
mdiAccessPoint,
mdiAccountLock,
mdiChatProcessing,
mdiChatQuestion,
mdiExportVariant,
Expand All @@ -10,11 +11,13 @@ import {
NetworkType,
getMatterNodeDiagnostics,
} from "../../../../../../data/matter";
import { getMatterLockInfo } from "../../../../../../data/matter-lock";
import type { HomeAssistant } from "../../../../../../types";
import { showMatterManageFabricsDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-manage-fabrics";
import { showMatterOpenCommissioningWindowDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-open-commissioning-window";
import { showMatterPingNodeDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-ping-node";
import { showMatterReinterviewNodeDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-reinterview-node";
import { showMatterLockManageDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-lock-manage";
import type { DeviceAction } from "../../../ha-config-device-page";

export const getMatterDeviceDefaultActions = (
Expand Down Expand Up @@ -46,12 +49,26 @@ export const getMatterDeviceActions = async (
hass: HomeAssistant,
device: DeviceRegistryEntry
): Promise<DeviceAction[]> => {
// eslint-disable-next-line no-console
console.log(
"[Matter Debug] getMatterDeviceActions called for device:",
device.id,
device.name
);

if (device.via_device_id !== null) {
// only show device actions for top level nodes (so not bridged)
// eslint-disable-next-line no-console
console.log(
"[Matter Debug] Skipping - device has via_device_id:",
device.via_device_id
);
return [];
}

const nodeDiagnostics = await getMatterNodeDiagnostics(hass, device.id);
// eslint-disable-next-line no-console
console.log("[Matter Debug] Node diagnostics:", nodeDiagnostics);

const actions: DeviceAction[] = [];

Expand Down Expand Up @@ -99,5 +116,32 @@ export const getMatterDeviceActions = async (
});
}

// Check if device is a lock and add lock management action
try {
// eslint-disable-next-line no-console
console.log(
"[Matter Lock Debug] Checking lock info for device:",
device.id
);
const lockInfo = await getMatterLockInfo(hass, device.id);
// eslint-disable-next-line no-console
console.log("[Matter Lock Debug] Lock info result:", lockInfo);
if (lockInfo.supports_user_management) {
actions.push({
label: hass.localize(
"ui.panel.config.matter.device_actions.manage_lock"
),
icon: mdiAccountLock,
action: () =>
showMatterLockManageDialog(el, {
device_id: device.id,
}),
});
}
} catch (err) {
// eslint-disable-next-line no-console
console.error("[Matter Lock Debug] Error getting lock info:", err);
}

return actions;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import {
mdiLock,
mdiLockOpen,
mdiLockAlert,
mdiAlertCircle,
mdiKeyAlert,
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../../components/ha-expansion-panel";
import "../../../../../../components/ha-svg-icon";
import type { DeviceRegistryEntry } from "../../../../../../data/device/device_registry";
import { SubscribeMixin } from "../../../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../../types";

interface LockEvent {
event_type: string;
timestamp: Date;
description: string;
}

const EVENT_ICONS: Record<string, string> = {
lock: mdiLock,
unlock: mdiLockOpen,
lock_jammed: mdiLockAlert,
lock_failure: mdiAlertCircle,
invalid_pin: mdiKeyAlert,
};

@customElement("ha-device-info-matter-lock")
export class HaDeviceInfoMatterLock extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;

@property({ attribute: false }) public device!: DeviceRegistryEntry;

@state() private _events: LockEvent[] = [];

@state() private _lockEntityId?: string;

@state() private _isLock = false;

public willUpdate(changedProperties: PropertyValues) {
super.willUpdate(changedProperties);
if (changedProperties.has("device") || changedProperties.has("hass")) {
this._findLockEntity();
}
if (changedProperties.has("hass") && this._lockEntityId) {
this._updateEvents();
}
}

private _findLockEntity(): void {
if (!this.hass || !this.device) {
return;
}

// Find the lock entity for this device using hass.entities
const entities = Object.values(this.hass.entities || {});
const lockEntity = entities.find(
(entity) =>
entity.device_id === this.device.id &&
entity.entity_id.startsWith("lock.")
);

this._lockEntityId = lockEntity?.entity_id;
this._isLock = !!this._lockEntityId;
}

private _updateEvents(): void {
if (!this._lockEntityId || !this.hass.states[this._lockEntityId]) {
return;
}

// Get the lock entity state
const lockState: HassEntity = this.hass.states[this._lockEntityId];

// Check for recent state changes
const events: LockEvent[] = [];

// Add current state as most recent event
if (lockState.state === "locked") {
events.push({
event_type: "lock",
timestamp: new Date(lockState.last_changed),
description: this.hass.localize(
"ui.panel.config.matter.lock.events.types.lock"
),
});
} else if (lockState.state === "unlocked") {
events.push({
event_type: "unlock",
timestamp: new Date(lockState.last_changed),
description: this.hass.localize(
"ui.panel.config.matter.lock.events.types.unlock"
),
});
} else if (lockState.state === "jammed") {
events.push({
event_type: "lock_jammed",
timestamp: new Date(lockState.last_changed),
description: this.hass.localize(
"ui.panel.config.matter.lock.events.types.lock_jammed"
),
});
}

// Look for event entities related to this lock using hass.entities
const allEntities = Object.values(this.hass.entities || {});
const eventEntities = allEntities.filter(
(entity) =>
entity.device_id === this.device.id &&
entity.entity_id.startsWith("event.")
);

for (const eventEntity of eventEntities) {
const eventState = this.hass.states[eventEntity.entity_id];
if (eventState && eventState.attributes.event_type) {
const eventType = eventState.attributes.event_type as string;
if (EVENT_ICONS[eventType]) {
events.push({
event_type: eventType,
timestamp: new Date(eventState.last_changed),
description:
this.hass.localize(
`ui.panel.config.matter.lock.events.types.${eventType}`

Check failure on line 128 in src/panels/config/devices/device-detail/integration-elements/matter/ha-device-info-matter-lock.ts

View workflow job for this annotation

GitHub Actions / Lint and check format

Argument of type '`ui.panel.config.matter.lock.events.types.${string}`' is not assignable to parameter of type 'LocalizeKeys'.
) || eventType,
});
}
}
}

// Sort by timestamp descending and take the first 10
this._events = events
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
.slice(0, 10);
}

protected render() {
if (!this._isLock) {
return nothing;
}

return html`
<ha-expansion-panel
.header=${this.hass.localize(
"ui.panel.config.matter.lock.events.title"
)}
>
${this._events.length === 0
? html`<p class="empty">
${this.hass.localize(
"ui.panel.config.matter.lock.events.no_events"
)}
</p>`
: html`
<div class="events-list">
${this._events.map(
(event) => html`
<div class="event-row">
<ha-svg-icon
.path=${EVENT_ICONS[event.event_type] || mdiLock}
></ha-svg-icon>
<div class="event-details">
<span class="event-description"
>${event.description}</span
>
<span class="event-time">
${this._formatTime(event.timestamp)}
</span>
</div>
</div>
`
)}
</div>
`}
</ha-expansion-panel>
`;
}

private _formatTime(date: Date): string {
return date.toLocaleString(this.hass.locale.language, {
dateStyle: "short",
timeStyle: "short",
});
}

static get styles(): CSSResultGroup {
return [
haStyle,
css`
ha-expansion-panel {
margin: 8px -16px 0;
--expansion-panel-summary-padding: 0 16px;
--expansion-panel-content-padding: 0 16px;
--ha-card-border-radius: var(--ha-border-radius-square);
}
.empty {
text-align: center;
color: var(--secondary-text-color);
padding: 16px;
}
.events-list {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px 0;
}
.event-row {
display: flex;
align-items: center;
gap: 12px;
}
.event-row ha-svg-icon {
color: var(--secondary-text-color);
--mdc-icon-size: 20px;
}
.event-details {
display: flex;
flex-direction: column;
flex: 1;
}
.event-description {
font-weight: 500;
}
.event-time {
font-size: 0.875em;
color: var(--secondary-text-color);
}
`,
];
}
}

declare global {
interface HTMLElementTagNameMap {
"ha-device-info-matter-lock": HaDeviceInfoMatterLock;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getMatterNodeDiagnostics } from "../../../../../../data/matter";
import { SubscribeMixin } from "../../../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../../types";
import "./ha-device-info-matter-lock";

@customElement("ha-device-info-matter")
export class HaDeviceInfoMatter extends SubscribeMixin(LitElement) {
Expand Down Expand Up @@ -124,6 +125,10 @@ export class HaDeviceInfoMatter extends SubscribeMixin(LitElement) {
>
</div>
</ha-expansion-panel>
<ha-device-info-matter-lock
.hass=${this.hass}
.device=${this.device}
></ha-device-info-matter-lock>
`;
}

Expand Down
Loading
Loading