mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
Merge v1.30.0: lock action in the opening info card
This commit is contained in:
@@ -11,7 +11,7 @@ PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory
|
|||||||
FILES_URL = "/houseplan_files/files"
|
FILES_URL = "/houseplan_files/files"
|
||||||
FILES_DIR = "houseplan/files"
|
FILES_DIR = "houseplan/files"
|
||||||
CONF_ADMIN_ONLY = "admin_only"
|
CONF_ADMIN_ONLY = "admin_only"
|
||||||
VERSION = "1.29.0"
|
VERSION = "1.30.0"
|
||||||
|
|
||||||
DEFAULT_CONFIG: dict = {
|
DEFAULT_CONFIG: dict = {
|
||||||
"spaces": [],
|
"spaces": [],
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -16,5 +16,5 @@
|
|||||||
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"single_config_entry": true,
|
"single_config_entry": true,
|
||||||
"version": "1.29.0"
|
"version": "1.30.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { launch } from './serve.mjs';
|
||||||
|
const { page, browser } = await launch();
|
||||||
|
const res = await page.evaluate(async () => {
|
||||||
|
const out = {};
|
||||||
|
const c = window.__card;
|
||||||
|
const sr = () => c.shadowRoot || c.renderRoot;
|
||||||
|
const calls = [];
|
||||||
|
c.hass = { ...c.hass, callService: (d, s, data) => calls.push([d, s, data.entity_id]) };
|
||||||
|
await c.updateComplete;
|
||||||
|
// добавить дверь с замком на f1
|
||||||
|
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== 'f1' ? s : ({
|
||||||
|
...s, openings: [{ id: 'op1', type: 'door', rx: 550, ry: 200, angle: 90, len_cm: 90, lock: 'lock.front_door' }] })) };
|
||||||
|
c.requestUpdate(); await c.updateComplete;
|
||||||
|
const op = c._spaceModel()?.openings?.[0] || { id: 'op1', type: 'door', rx: 550, ry: 200, len_cm: 90, lock: 'lock.front_door' };
|
||||||
|
out.hasLockedDoor = !!op.lock;
|
||||||
|
const lockId = op.lock;
|
||||||
|
// 1) замок закрыт → кнопка "Unlock"
|
||||||
|
c.hass = { ...c.hass, states: { ...c.hass.states, [lockId]: { state: 'locked', attributes: {} } } };
|
||||||
|
c._openingInfo = op; await c.updateComplete;
|
||||||
|
let btn = sr().querySelector('.btn.lockact');
|
||||||
|
out.unlockBtnShown = !!btn && !btn.disabled;
|
||||||
|
out.unlockBtnDanger = btn?.classList.contains('warn');
|
||||||
|
btn.click();
|
||||||
|
out.unlockCalled = JSON.stringify(calls.at(-1)) === JSON.stringify(['lock', 'unlock', lockId]);
|
||||||
|
// 2) замок открыт → кнопка "Lock"
|
||||||
|
c.hass = { ...c.hass, states: { ...c.hass.states, [lockId]: { state: 'unlocked', attributes: {} } } };
|
||||||
|
await c.updateComplete;
|
||||||
|
btn = sr().querySelector('.btn.lockact');
|
||||||
|
btn.click();
|
||||||
|
out.lockCalled = JSON.stringify(calls.at(-1)) === JSON.stringify(['lock', 'lock', lockId]);
|
||||||
|
// 3) переходное состояние → кнопка задизейблена, сервис не зовётся
|
||||||
|
c.hass = { ...c.hass, states: { ...c.hass.states, [lockId]: { state: 'unlocking', attributes: {} } } };
|
||||||
|
await c.updateComplete;
|
||||||
|
btn = sr().querySelector('.btn.lockact');
|
||||||
|
out.pendingDisabled = !!btn && btn.disabled;
|
||||||
|
// 4) недоступен → кнопки нет
|
||||||
|
c.hass = { ...c.hass, states: { ...c.hass.states, [lockId]: { state: 'unavailable', attributes: {} } } };
|
||||||
|
await c.updateComplete;
|
||||||
|
out.noBtnWhenUnavailable = !sr().querySelector('.btn.lockact');
|
||||||
|
// 5) тап по иконке на плане по-прежнему НЕ дёргает замок (hard block)
|
||||||
|
const n = calls.length;
|
||||||
|
const dev = c._devices.find((d) => d.domains?.includes('lock'));
|
||||||
|
out.planTapStillBlocked = true;
|
||||||
|
if (dev) {
|
||||||
|
c._clickDevice(new MouseEvent('click'), dev); await c.updateComplete;
|
||||||
|
out.planTapStillBlocked = !calls.slice(n).some((x) => x[0] === 'lock' && (x[1] === 'lock' || x[1] === 'unlock'));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
console.log(JSON.stringify(res, null, 1));
|
||||||
|
await browser.close();
|
||||||
File diff suppressed because one or more lines are too long
Vendored
+23
-3
File diff suppressed because one or more lines are too long
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.30.0 — 2026-07-22 (lock action in the opening info card)
|
||||||
|
- The door/window info card (View mode) now offers an explicit **Unlock/Lock
|
||||||
|
button** when a lock entity is bound and available. Unlock is styled red as a
|
||||||
|
security-sensitive action; the button is disabled during locking/unlocking and
|
||||||
|
hidden when the lock is unavailable.
|
||||||
|
- The security rule is untouched: **tapping a lock icon on the plan still never
|
||||||
|
toggles it** — the action lives only behind a deliberate, clearly labeled
|
||||||
|
button, same interaction contract as HA's more-info dialog.
|
||||||
|
|
||||||
## v1.29.0 — 2026-07-22 (the "new device" flag)
|
## v1.29.0 — 2026-07-22 (the "new device" flag)
|
||||||
- **Devices that appear in HA after installation no longer show up silently**:
|
- **Devices that appear in HA after installation no longer show up silently**:
|
||||||
an auto-placed device (or light group) gets a big red dot at the top-right of
|
an auto-placed device (or light group) gets a big red dot at the top-right of
|
||||||
|
|||||||
@@ -140,6 +140,10 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
|||||||
(explicit ripple color still wins); off/white lights unchanged [auto]
|
(explicit ripple color still wins); off/white lights unchanged [auto]
|
||||||
- [ ] Alarm pulse (v1.27.0): leak/smoke/gas/CO/siren in 'on' pulse a red ring over any
|
- [ ] Alarm pulse (v1.27.0): leak/smoke/gas/CO/siren in 'on' pulse a red ring over any
|
||||||
display mode; clears on 'off'; unavailable never alarms [auto]; reduced-motion static
|
display mode; clears on 'off'; unavailable never alarms [auto]; reduced-motion static
|
||||||
|
- [ ] Lock action (v1.30.0): opening info card (View) shows Unlock (red) when
|
||||||
|
locked / Lock when unlocked; button calls the lock service; disabled while
|
||||||
|
locking/unlocking; hidden when unavailable; plan-icon tap still never
|
||||||
|
toggles a lock [auto]
|
||||||
- [ ] New-device flag (v1.29.0): a device added to HA after install gets a big red
|
- [ ] New-device flag (v1.29.0): a device added to HA after install gets a big red
|
||||||
dot top-right of its icon (all clients); opening its editor clears it
|
dot top-right of its icon (all clients); opening its editor clears it
|
||||||
everywhere; upgrade/first-run seeds the baseline silently — no dot flood [auto]
|
everywhere; upgrade/first-run seeds the baseline silently — no dot flood [auto]
|
||||||
|
|||||||
+3
-1
@@ -22,7 +22,9 @@ obvious at a glance:
|
|||||||
|
|
||||||
Allowed: pan/zoom (wheel, pinch, buttons), switching spaces, device tap
|
Allowed: pan/zoom (wheel, pinch, buttons), switching spaces, device tap
|
||||||
(info / more-info / toggle per settings), long-press → info card, opening tap →
|
(info / more-info / toggle per settings), long-press → info card, opening tap →
|
||||||
door/lock info card, room tap → HA area, hover tooltips (name, temperature, signal).
|
door/lock info card (with an explicit Unlock/Lock button when a lock is bound —
|
||||||
|
the only way to operate a lock from the card; plan-icon taps never toggle locks),
|
||||||
|
room tap → HA area, hover tooltips (name, temperature, signal).
|
||||||
|
|
||||||
Removed from this mode (they move, not die):
|
Removed from this mode (they move, not die):
|
||||||
- icon dragging ("drag anywhere", v1.9 — consciously reversed),
|
- icon dragging ("drag anywhere", v1.9 — consciously reversed),
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "houseplan-card",
|
"name": "houseplan-card",
|
||||||
"version": "1.29.0",
|
"version": "1.30.0",
|
||||||
"description": "Interactive house plan Lovelace card for Home Assistant",
|
"description": "Interactive house plan Lovelace card for Home Assistant",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+23
-1
@@ -32,7 +32,7 @@ import './space-card';
|
|||||||
import { cardStyles } from './styles';
|
import { cardStyles } from './styles';
|
||||||
import { langOf, t, type I18nKey } from './i18n';
|
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_KEY = 'houseplan_card_layout_v1';
|
||||||
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
||||||
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
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 {
|
private _renderOpeningInfoCard(): TemplateResult {
|
||||||
const o = this._openingInfo!;
|
const o = this._openingInfo!;
|
||||||
const cSt = o.contact ? this.hass.states[o.contact]?.state : null;
|
const cSt = o.contact ? this.hass.states[o.contact]?.state : null;
|
||||||
@@ -2854,6 +2864,18 @@ class HouseplanCard extends LitElement {
|
|||||||
: this._t('opening.state_unknown'),
|
: this._t('opening.state_unknown'),
|
||||||
lSt === 'locked' ? 'ok' : 'warn')
|
lSt === 'locked' ? 'ok' : 'warn')
|
||||||
: nothing}
|
: 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}
|
${!o.contact && !o.lock ? html`<p class="muted">${this._t('opening.no_entities')}</p>` : nothing}
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
+4
-1
@@ -245,5 +245,8 @@
|
|||||||
"mode.devices": "Devices",
|
"mode.devices": "Devices",
|
||||||
"display.value": "Value instead of an icon",
|
"display.value": "Value instead of an icon",
|
||||||
"marker.subarea": "no area, manual",
|
"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
@@ -245,5 +245,8 @@
|
|||||||
"mode.devices": "Устройства",
|
"mode.devices": "Устройства",
|
||||||
"display.value": "Значение вместо иконки",
|
"display.value": "Значение вместо иконки",
|
||||||
"marker.subarea": "без зоны, вручную",
|
"marker.subarea": "без зоны, вручную",
|
||||||
"device.new": "Новое устройство — откройте его редактор, чтобы снять отметку"
|
"device.new": "Новое устройство — откройте его редактор, чтобы снять отметку",
|
||||||
|
"opening.unlock_action": "Открыть замок",
|
||||||
|
"opening.lock_action": "Закрыть замок",
|
||||||
|
"opening.lock_pending": "Выполняется…"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,6 +286,18 @@ export const cardStyles = css`
|
|||||||
.oplock.locked { color: #66d17a; border-color: #66d17a; }
|
.oplock.locked { color: #66d17a; border-color: #66d17a; }
|
||||||
.oplock.unlocked { color: var(--hp-open); border-color: var(--hp-open); }
|
.oplock.unlocked { color: var(--hp-open); border-color: var(--hp-open); }
|
||||||
.oplock.unknown { color: var(--hp-muted); }
|
.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 {
|
.oprow {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user