mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Add more info lock (#15995)
* Add more info lock * Use same height for pending state * Fix attributes * Add unlocking/locking to switch * Improve code support
This commit is contained in:
parent
922e95b895
commit
7bc2ca3b65
20
src/data/lock.ts
Normal file
20
src/data/lock.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
|
||||
export const FORMAT_TEXT = "text";
|
||||
export const FORMAT_NUMBER = "number";
|
||||
|
||||
export const enum LockEntityFeature {
|
||||
OPEN = 1,
|
||||
}
|
||||
|
||||
interface LockEntityAttributes extends HassEntityAttributeBase {
|
||||
code_format?: string;
|
||||
changed_by?: string | null;
|
||||
}
|
||||
|
||||
export interface LockEntity extends HassEntityBase {
|
||||
attributes: LockEntityAttributes;
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
import { mdiCheck, mdiClose } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-control-button";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../components/ha-textfield";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { HassDialog } from "../../../make-dialog-manager";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-control-button";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../components/ha-textfield";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { HassDialog } from "../make-dialog-manager";
|
||||
import { EnterCodeDialogParams } from "./show-enter-code-dialog";
|
||||
|
||||
const BUTTONS = [
|
||||
@ -72,7 +73,8 @@ export class DialogEnterCode
|
||||
}
|
||||
|
||||
private _inputValueChange(e) {
|
||||
const val = (e.currentTarget! as any).value;
|
||||
const field = e.currentTarget as HaTextField;
|
||||
const val = field.value;
|
||||
this._showClearButton = !!val;
|
||||
}
|
||||
|
||||
@ -97,6 +99,7 @@ export class DialogEnterCode
|
||||
id="code"
|
||||
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
|
||||
type="password"
|
||||
pattern=${ifDefined(this._dialogParams.codePattern)}
|
||||
input-mode="text"
|
||||
></ha-textfield>
|
||||
<ha-button slot="secondaryAction" dialogAction="cancel">
|
@ -1,7 +1,8 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export interface EnterCodeDialogParams {
|
||||
codeFormat: "text" | "number";
|
||||
codePattern?: string;
|
||||
submitText?: string;
|
||||
cancelText?: string;
|
||||
title?: string;
|
@ -14,7 +14,7 @@ import {
|
||||
} from "../../../../data/alarm_control_panel";
|
||||
import { UNAVAILABLE } from "../../../../data/entity";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { showEnterCodeDialogDialog } from "./show-enter-code-dialog";
|
||||
import { showEnterCodeDialogDialog } from "../../../enter-code/show-enter-code-dialog";
|
||||
|
||||
@customElement("ha-more-info-alarm_control_panel-modes")
|
||||
export class HaMoreInfoAlarmControlPanelModes extends LitElement {
|
||||
|
@ -0,0 +1,199 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { domainIcon } from "../../../../common/entity/domain_icon";
|
||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||
import "../../../../components/ha-control-button";
|
||||
import "../../../../components/ha-control-switch";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../../data/entity";
|
||||
import { forwardHaptic } from "../../../../data/haptics";
|
||||
import { LockEntity } from "../../../../data/lock";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { showEnterCodeDialogDialog } from "../../../enter-code/show-enter-code-dialog";
|
||||
|
||||
@customElement("ha-more-info-lock-toggle")
|
||||
export class HaMoreInfoLockToggle extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: LockEntity;
|
||||
|
||||
@state() private _isOn = false;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("stateObj")) {
|
||||
this._isOn =
|
||||
this.stateObj.state === "locked" || this.stateObj.state === "locking";
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
const checked = ev.target.checked as boolean;
|
||||
|
||||
if (checked) {
|
||||
this._turnOn();
|
||||
} else {
|
||||
this._turnOff();
|
||||
}
|
||||
}
|
||||
|
||||
private async _turnOn() {
|
||||
this._isOn = true;
|
||||
try {
|
||||
await this._callService(true);
|
||||
} catch (err) {
|
||||
this._isOn = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _turnOff() {
|
||||
this._isOn = false;
|
||||
try {
|
||||
await this._callService(false);
|
||||
} catch (err) {
|
||||
this._isOn = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async _callService(turnOn: boolean): Promise<void> {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return;
|
||||
}
|
||||
forwardHaptic("light");
|
||||
|
||||
let code: string | undefined;
|
||||
|
||||
if (this.stateObj.attributes.code_format) {
|
||||
const response = await showEnterCodeDialogDialog(this, {
|
||||
codeFormat: "text",
|
||||
codePattern: this.stateObj.attributes.code_format,
|
||||
title: this.hass.localize(
|
||||
`ui.dialogs.more_info_control.lock.${turnOn ? "lock" : "unlock"}`
|
||||
),
|
||||
submitText: this.hass.localize(
|
||||
`ui.dialogs.more_info_control.lock.${turnOn ? "lock" : "unlock"}`
|
||||
),
|
||||
});
|
||||
if (response == null) {
|
||||
throw new Error("cancel");
|
||||
}
|
||||
code = response;
|
||||
}
|
||||
|
||||
await this.hass.callService("lock", turnOn ? "lock" : "unlock", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
code,
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const locking = this.stateObj.state === "locking";
|
||||
const unlocking = this.stateObj.state === "unlocking";
|
||||
|
||||
const color = stateColorCss(this.stateObj);
|
||||
|
||||
const onIcon = domainIcon(
|
||||
"lock",
|
||||
this.stateObj,
|
||||
locking ? "locking" : "locked"
|
||||
);
|
||||
|
||||
const offIcon = domainIcon(
|
||||
"lock",
|
||||
this.stateObj,
|
||||
unlocking ? "unlocking" : "unlocked"
|
||||
);
|
||||
|
||||
if (this.stateObj.state === UNKNOWN) {
|
||||
return html`
|
||||
<div class="buttons">
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.lock.lock"
|
||||
)}
|
||||
@click=${this._turnOn}
|
||||
>
|
||||
<ha-svg-icon .path=${onIcon}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.lock.unlock"
|
||||
)}
|
||||
@click=${this._turnOff}
|
||||
>
|
||||
<ha-svg-icon .path=${offIcon}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-control-switch
|
||||
.pathOn=${onIcon}
|
||||
.pathOff=${offIcon}
|
||||
vertical
|
||||
reversed
|
||||
.checked=${this._isOn}
|
||||
@change=${this._valueChanged}
|
||||
.ariaLabel=${this.hass.localize("ui.dialogs.more_info_control.toggle")}
|
||||
style=${styleMap({
|
||||
"--control-switch-on-color": color,
|
||||
"--control-switch-off-color": color,
|
||||
})}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
</ha-control-switch>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-control-switch {
|
||||
height: 45vh;
|
||||
max-height: 320px;
|
||||
min-height: 200px;
|
||||
--control-switch-thickness: 100px;
|
||||
--control-switch-border-radius: 24px;
|
||||
--control-switch-padding: 6px;
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100px;
|
||||
height: 45vh;
|
||||
max-height: 320px;
|
||||
min-height: 200px;
|
||||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
ha-control-button {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
--control-button-border-radius: 18px;
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
ha-control-button.active {
|
||||
--control-button-icon-color: white;
|
||||
--control-button-background-color: var(--color);
|
||||
--control-button-background-opacity: 1;
|
||||
}
|
||||
ha-control-button:not(:last-child) {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-lock-toggle": HaMoreInfoLockToggle;
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
|
||||
"fan",
|
||||
"input_boolean",
|
||||
"light",
|
||||
"lock",
|
||||
"siren",
|
||||
"switch",
|
||||
];
|
||||
|
@ -7,8 +7,8 @@ import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import "../../../components/ha-outlined-button";
|
||||
import { AlarmControlPanelEntity } from "../../../data/alarm_control_panel";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showEnterCodeDialogDialog } from "../../enter-code/show-enter-code-dialog";
|
||||
import "../components/alarm_control_panel/ha-more-info-alarm_control_panel-modes";
|
||||
import { showEnterCodeDialogDialog } from "../components/alarm_control_panel/show-enter-code-dialog";
|
||||
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
|
||||
import "../components/ha-more-info-state-header";
|
||||
|
||||
|
@ -1,43 +1,156 @@
|
||||
import "@material/mwc-button";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "@material/web/iconbutton/outlined-icon-button";
|
||||
import { mdiDoorOpen, mdiLock, mdiLockOff } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { domainIcon } from "../../../common/entity/domain_icon";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-attributes";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { LockEntity, LockEntityFeature } from "../../../data/lock";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showEnterCodeDialogDialog } from "../../enter-code/show-enter-code-dialog";
|
||||
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
|
||||
import "../components/ha-more-info-state-header";
|
||||
import "../components/lock/ha-more-info-lock-toggle";
|
||||
|
||||
@customElement("more-info-lock")
|
||||
class MoreInfoLock extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
@property({ attribute: false }) public stateObj?: LockEntity;
|
||||
|
||||
@query("ha-textfield") private _textfield?: HaTextField;
|
||||
private async _open() {
|
||||
this._callService("open");
|
||||
}
|
||||
|
||||
private async _lock() {
|
||||
this._callService("lock");
|
||||
}
|
||||
|
||||
private async _unlock() {
|
||||
this._callService("unlock");
|
||||
}
|
||||
|
||||
private async _callService(service: "open" | "lock" | "unlock") {
|
||||
let code: string | undefined;
|
||||
|
||||
if (this.stateObj!.attributes.code_format) {
|
||||
const response = await showEnterCodeDialogDialog(this, {
|
||||
codeFormat: "text",
|
||||
codePattern: this.stateObj!.attributes.code_format,
|
||||
title: this.hass.localize(
|
||||
`ui.dialogs.more_info_control.lock.${service}`
|
||||
),
|
||||
submitText: this.hass.localize(
|
||||
`ui.dialogs.more_info_control.lock.${service}`
|
||||
),
|
||||
});
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
code = response;
|
||||
}
|
||||
|
||||
this.hass.callService("lock", service, {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
code,
|
||||
});
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const supportsOpen = supportsFeature(this.stateObj, LockEntityFeature.OPEN);
|
||||
|
||||
const color = stateColorCss(this.stateObj);
|
||||
const style = {
|
||||
"--icon-color": color,
|
||||
};
|
||||
|
||||
const isJammed = this.stateObj.state === "jammed";
|
||||
|
||||
return html`
|
||||
${this.stateObj.attributes.code_format
|
||||
? html`<div class="code">
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize("ui.card.lock.code")}
|
||||
.pattern=${this.stateObj.attributes.code_format}
|
||||
type="password"
|
||||
></ha-textfield>
|
||||
${this.stateObj.state === "locked"
|
||||
? html`<mwc-button
|
||||
@click=${this._callService}
|
||||
data-service="unlock"
|
||||
>${this.hass.localize("ui.card.lock.unlock")}</mwc-button
|
||||
>`
|
||||
: html`<mwc-button @click=${this._callService} data-service="lock"
|
||||
>${this.hass.localize("ui.card.lock.lock")}</mwc-button
|
||||
>`}
|
||||
</div>`
|
||||
: ""}
|
||||
<ha-more-info-state-header
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-more-info-state-header>
|
||||
<div class="controls" style=${styleMap(style)}>
|
||||
${
|
||||
this.stateObj.state === "jammed"
|
||||
? html`
|
||||
<div class="status">
|
||||
<span></span>
|
||||
<div class="icon">
|
||||
<ha-svg-icon
|
||||
.path=${domainIcon("lock", this.stateObj)}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-more-info-lock-toggle
|
||||
.stateObj=${this.stateObj}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</ha-more-info-lock-toggle>
|
||||
`
|
||||
}
|
||||
${
|
||||
supportsOpen || isJammed
|
||||
? html`
|
||||
<div class="buttons">
|
||||
${supportsOpen
|
||||
? html`
|
||||
<md-outlined-icon-button
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.title=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.lock.open"
|
||||
)}
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.lock.open"
|
||||
)}
|
||||
@click=${this._open}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDoorOpen}></ha-svg-icon>
|
||||
</md-outlined-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
${isJammed
|
||||
? html`
|
||||
<md-outlined-icon-button
|
||||
.title=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.lock.lock"
|
||||
)}
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.lock.lock"
|
||||
)}
|
||||
@click=${this._lock}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiLock}></ha-svg-icon>
|
||||
</md-outlined-icon-button>
|
||||
<md-outlined-icon-button
|
||||
.title=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.lock.unlock"
|
||||
)}
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.lock.unlock"
|
||||
)}
|
||||
@click=${this._unlock}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiLockOff}></ha-svg-icon>
|
||||
</md-outlined-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<ha-attributes
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@ -46,33 +159,64 @@ class MoreInfoLock extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _callService(ev) {
|
||||
const service = ev.target.getAttribute("data-service");
|
||||
const data = {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
code: this._textfield?.value,
|
||||
};
|
||||
this.hass.callService("lock", service, data);
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
moreInfoControlStyle,
|
||||
css`
|
||||
md-outlined-icon-button {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-on-surface: var(--secondary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
--md-sys-color-on-surface-rgb: var(--rgb-secondary-text-color);
|
||||
--md-sys-color-outline: var(--secondary-text-color);
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 45vh;
|
||||
max-height: 320px;
|
||||
min-height: 200px;
|
||||
}
|
||||
.status .icon {
|
||||
position: relative;
|
||||
--mdc-icon-size: 80px;
|
||||
animation: pulse 1s infinite;
|
||||
color: var(--icon-color);
|
||||
border-radius: 50%;
|
||||
width: 144px;
|
||||
height: 144px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.status .icon::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: var(--icon-color);
|
||||
transition: background-color 180ms ease-in-out;
|
||||
opacity: 0.2;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.code {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-attributes {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -18,10 +18,10 @@ import {
|
||||
ALARM_MODES,
|
||||
} from "../../../data/alarm_control_panel";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { showEnterCodeDialogDialog } from "../../../dialogs/more-info/components/alarm_control_panel/show-enter-code-dialog";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceTileFeature, LovelaceTileFeatureEditor } from "../types";
|
||||
import { AlarmModesFileFeatureConfig } from "./types";
|
||||
import { showEnterCodeDialogDialog } from "../../../dialogs/enter-code/show-enter-code-dialog";
|
||||
|
||||
export const supportsAlarmModesTileFeature = (stateObj: HassEntity) => {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
|
@ -978,6 +978,11 @@
|
||||
"disarm_action": "Disarm",
|
||||
"arm_title": "Arm",
|
||||
"arm_action": "Arm"
|
||||
},
|
||||
"lock": {
|
||||
"open": "Open",
|
||||
"lock": "Lock",
|
||||
"unlock": "Unlock"
|
||||
}
|
||||
},
|
||||
"entity_registry": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user