feat v1.30.0: explicit Unlock/Lock action in the opening info card

- View-mode door/window info card gets an Unlock (red) / Lock button when a
  lock entity is bound and available; disabled while locking/unlocking,
  hidden when unavailable
- plan-icon taps still never toggle locks (hard block untouched);
  smoke_lock_action.mjs covers the full matrix incl. the block
- TESTING.md, CHANGELOG, UX-MODES same-commit
This commit is contained in:
Matysh
2026-07-22 11:11:18 +03:00
parent 29c5a89aad
commit 6ee09d3750
14 changed files with 182 additions and 16 deletions
+23 -1
View File
@@ -32,7 +32,7 @@ import './space-card';
import { cardStyles } from './styles';
import { langOf, t, type I18nKey } from './i18n';
const CARD_VERSION = '1.29.0';
const CARD_VERSION = '1.30.0';
const LS_KEY = 'houseplan_card_layout_v1';
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
const LS_ZOOM = 'houseplan_card_zoom_v1';
@@ -2826,6 +2826,16 @@ class HouseplanCard extends LitElement {
})}`;
}
/**
* Explicit lock/unlock from the opening info card. This does NOT violate the
* "locks never toggle from the plan" rule: that rule guards against ACCIDENTAL
* taps on plan icons; here the user has opened the info card and pressed a
* clearly labeled action button — same interaction contract as HA's more-info.
*/
private _lockAction(entityId: string, action: 'lock' | 'unlock'): void {
this.hass?.callService?.('lock', action, { entity_id: entityId });
}
private _renderOpeningInfoCard(): TemplateResult {
const o = this._openingInfo!;
const cSt = o.contact ? this.hass.states[o.contact]?.state : null;
@@ -2854,6 +2864,18 @@ class HouseplanCard extends LitElement {
: this._t('opening.state_unknown'),
lSt === 'locked' ? 'ok' : 'warn')
: nothing}
${o.lock && (lSt === 'locked' || ['unlocked', 'open'].includes(String(lSt)))
? html`<button
class="btn lockact ${lSt === 'locked' ? 'warn' : ''}"
@click=${() => this._lockAction(o.lock!, lSt === 'locked' ? 'unlock' : 'lock')}>
<ha-icon icon=${lSt === 'locked' ? 'mdi:lock-open-variant' : 'mdi:lock'}></ha-icon>
${this._t(lSt === 'locked' ? 'opening.unlock_action' : 'opening.lock_action')}
</button>`
: o.lock && ['locking', 'unlocking'].includes(String(lSt))
? html`<button class="btn lockact" disabled>
<ha-icon icon="mdi:timer-sand"></ha-icon>${this._t('opening.lock_pending')}
</button>`
: nothing}
${!o.contact && !o.lock ? html`<p class="muted">${this._t('opening.no_entities')}</p>` : nothing}
</div>
<div class="row">
+4 -1
View File
@@ -245,5 +245,8 @@
"mode.devices": "Devices",
"display.value": "Value instead of an icon",
"marker.subarea": "no area, manual",
"device.new": "New device — open its editor to dismiss"
"device.new": "New device — open its editor to dismiss",
"opening.unlock_action": "Unlock",
"opening.lock_action": "Lock",
"opening.lock_pending": "Working…"
}
+4 -1
View File
@@ -245,5 +245,8 @@
"mode.devices": "Устройства",
"display.value": "Значение вместо иконки",
"marker.subarea": "без зоны, вручную",
"device.new": "Новое устройство — откройте его редактор, чтобы снять отметку"
"device.new": "Новое устройство — откройте его редактор, чтобы снять отметку",
"opening.unlock_action": "Открыть замок",
"opening.lock_action": "Закрыть замок",
"opening.lock_pending": "Выполняется…"
}
+12
View File
@@ -286,6 +286,18 @@ export const cardStyles = css`
.oplock.locked { color: #66d17a; border-color: #66d17a; }
.oplock.unlocked { color: var(--hp-open); border-color: var(--hp-open); }
.oplock.unknown { color: var(--hp-muted); }
.btn.lockact {
width: 100%;
justify-content: center;
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
}
.btn.lockact.warn {
color: var(--error-color, #d33);
border-color: var(--error-color, #d33);
}
.oprow {
display: flex;
align-items: center;