mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
Add more info alarm control panel (#15893)
* Add more info alarm control panel * Improve buttons sizes * Add triggered, arming and pending state * Add keypad * Improve alarm code dialog * Fix code condition * Clean code * Fix mode selection with code * Use nothing
This commit is contained in:
parent
3a700aebcc
commit
48c74c8660
@ -85,6 +85,7 @@ export class HaControlButton extends LitElement {
|
|||||||
--control-button-background-opacity: 0.2;
|
--control-button-background-opacity: 0.2;
|
||||||
--control-button-border-radius: 10px;
|
--control-button-border-radius: 10px;
|
||||||
--mdc-icon-size: 20px;
|
--mdc-icon-size: 20px;
|
||||||
|
color: var(--primary-text-color);
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
@ -110,6 +111,8 @@ export class HaControlButton extends LitElement {
|
|||||||
--mdc-ripple-color: var(--control-button-background-color);
|
--mdc-ripple-color: var(--control-button-background-color);
|
||||||
/* For safari border-radius overflow */
|
/* For safari border-radius overflow */
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
.button::before {
|
.button::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
@ -100,10 +100,11 @@ export class HaControlSelect extends LitElement {
|
|||||||
|
|
||||||
private _handleKeydown(ev: KeyboardEvent) {
|
private _handleKeydown(ev: KeyboardEvent) {
|
||||||
if (!this.options || this._activeIndex == null || this.disabled) return;
|
if (!this.options || this._activeIndex == null || this.disabled) return;
|
||||||
|
const value = this.options[this._activeIndex].value;
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
case " ":
|
case " ":
|
||||||
this.value = this.options[this._activeIndex].value;
|
this.value = value;
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
fireEvent(this, "value-changed", { value });
|
||||||
break;
|
break;
|
||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
case "ArrowLeft":
|
case "ArrowLeft":
|
||||||
@ -132,7 +133,7 @@ export class HaControlSelect extends LitElement {
|
|||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
const value = (ev.target as any).value;
|
const value = (ev.target as any).value;
|
||||||
this.value = value;
|
this.value = value;
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
fireEvent(this, "value-changed", { value });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleOptionMouseDown(ev: MouseEvent) {
|
private _handleOptionMouseDown(ev: MouseEvent) {
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
HassEntityAttributeBase,
|
||||||
|
HassEntityBase,
|
||||||
|
} from "home-assistant-js-websocket";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
export const FORMAT_TEXT = "text";
|
export const FORMAT_TEXT = "text";
|
||||||
@ -12,6 +16,16 @@ export const enum AlarmControlPanelEntityFeature {
|
|||||||
ARM_VACATION = 32,
|
ARM_VACATION = 32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AlarmControlPanelEntityAttributes extends HassEntityAttributeBase {
|
||||||
|
code_format?: "text" | "number";
|
||||||
|
changed_by?: string | null;
|
||||||
|
code_arm_required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlarmControlPanelEntity extends HassEntityBase {
|
||||||
|
attributes: AlarmControlPanelEntityAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
export const callAlarmAction = (
|
export const callAlarmAction = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entity: string,
|
entity: string,
|
||||||
|
@ -0,0 +1,248 @@
|
|||||||
|
import "@material/web/button/filled-button";
|
||||||
|
import "@material/web/iconbutton/filled-icon-button";
|
||||||
|
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 { EnterCodeDialogParams } from "./show-enter-code-dialog";
|
||||||
|
|
||||||
|
const BUTTONS = [
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5",
|
||||||
|
"6",
|
||||||
|
"7",
|
||||||
|
"8",
|
||||||
|
"9",
|
||||||
|
"0",
|
||||||
|
"clear",
|
||||||
|
"submit",
|
||||||
|
];
|
||||||
|
|
||||||
|
@customElement("dialog-enter-code")
|
||||||
|
export class DialogEnterCode
|
||||||
|
extends LitElement
|
||||||
|
implements HassDialog<EnterCodeDialogParams>
|
||||||
|
{
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _dialogParams?: EnterCodeDialogParams;
|
||||||
|
|
||||||
|
@query("#code") private _input?: HaTextField;
|
||||||
|
|
||||||
|
@state() private _showClearButton = false;
|
||||||
|
|
||||||
|
public async showDialog(dialogParams: EnterCodeDialogParams): Promise<void> {
|
||||||
|
this._dialogParams = dialogParams;
|
||||||
|
await this.updateComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog(): void {
|
||||||
|
if (this._dialogParams?.cancel) {
|
||||||
|
this._dialogParams.cancel();
|
||||||
|
}
|
||||||
|
this._dialogParams = undefined;
|
||||||
|
this._showClearButton = false;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _submit(): void {
|
||||||
|
this._dialogParams?.submit?.(this._input?.value ?? "");
|
||||||
|
this._dialogParams = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _numberClick(e: MouseEvent): void {
|
||||||
|
const val = (e.currentTarget! as any).value;
|
||||||
|
this._input!.value = this._input!.value + val;
|
||||||
|
this._showClearButton = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clear(): void {
|
||||||
|
this._input!.value = "";
|
||||||
|
this._showClearButton = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _inputValueChange(e) {
|
||||||
|
const val = (e.currentTarget! as any).value;
|
||||||
|
this._showClearButton = !!val;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._dialogParams || !this.hass) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isText = this._dialogParams.codeFormat === "text";
|
||||||
|
|
||||||
|
if (isText) {
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
defaultAction="ignore"
|
||||||
|
.heading=${this._dialogParams.title ??
|
||||||
|
this.hass.localize("ui.dialogs.enter_code.title")}
|
||||||
|
>
|
||||||
|
<ha-textfield
|
||||||
|
class="input"
|
||||||
|
dialogInitialFocus
|
||||||
|
id="code"
|
||||||
|
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
|
||||||
|
type="password"
|
||||||
|
input-mode="text"
|
||||||
|
></ha-textfield>
|
||||||
|
<ha-button @click=${this.closeDialog} slot="secondaryAction">
|
||||||
|
${this._dialogParams.cancelText ??
|
||||||
|
this.hass.localize("ui.common.cancel")}
|
||||||
|
</ha-button>
|
||||||
|
<ha-button @click=${this._submit} slot="primaryAction">
|
||||||
|
${this._dialogParams.submitText ??
|
||||||
|
this.hass.localize("ui.common.submit")}
|
||||||
|
</ha-button>
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
.heading=${createCloseHeading(
|
||||||
|
this.hass,
|
||||||
|
this._dialogParams.title ?? "Enter code"
|
||||||
|
)}
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
hideActions
|
||||||
|
>
|
||||||
|
<div class="container">
|
||||||
|
<ha-textfield
|
||||||
|
@input=${this._inputValueChange}
|
||||||
|
id="code"
|
||||||
|
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
|
||||||
|
type="password"
|
||||||
|
input-mode="numeric"
|
||||||
|
></ha-textfield>
|
||||||
|
<div class="keypad">
|
||||||
|
${BUTTONS.map((value) =>
|
||||||
|
value === ""
|
||||||
|
? html`<span></span>`
|
||||||
|
: value === "clear"
|
||||||
|
? html`
|
||||||
|
<ha-control-button
|
||||||
|
@click=${this._clear}
|
||||||
|
class="clear"
|
||||||
|
.disabled=${!this._showClearButton}
|
||||||
|
.label=${this.hass!.localize("ui.common.clear")}
|
||||||
|
>
|
||||||
|
<ha-svg-icon path=${mdiClose}></ha-svg-icon>
|
||||||
|
</ha-control-button>
|
||||||
|
`
|
||||||
|
: value === "submit"
|
||||||
|
? html`
|
||||||
|
<ha-control-button
|
||||||
|
@click=${this._submit}
|
||||||
|
class="submit"
|
||||||
|
.label=${this._dialogParams!.submitText ??
|
||||||
|
this.hass!.localize("ui.common.submit")}
|
||||||
|
>
|
||||||
|
<ha-svg-icon path=${mdiCheck}></ha-svg-icon>
|
||||||
|
</ha-control-button>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<ha-control-button
|
||||||
|
.value=${value}
|
||||||
|
@click=${this._numberClick}
|
||||||
|
.label=${value}
|
||||||
|
>
|
||||||
|
${value}
|
||||||
|
</ha-control-button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
ha-dialog {
|
||||||
|
--mdc-dialog-heading-ink-color: var(--primary-text-color);
|
||||||
|
--mdc-dialog-content-ink-color: var(--primary-text-color);
|
||||||
|
/* Place above other dialogs */
|
||||||
|
--dialog-z-index: 104;
|
||||||
|
}
|
||||||
|
ha-textfield {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.keypad {
|
||||||
|
--keypad-columns: 3;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--keypad-columns), auto);
|
||||||
|
grid-auto-rows: auto;
|
||||||
|
grid-gap: 24px;
|
||||||
|
justify-items: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.clear {
|
||||||
|
grid-row-start: 4;
|
||||||
|
grid-column-start: 0;
|
||||||
|
}
|
||||||
|
@media all and (max-height: 450px) {
|
||||||
|
.keypad {
|
||||||
|
--keypad-columns: 6;
|
||||||
|
}
|
||||||
|
.clear {
|
||||||
|
grid-row-start: 1;
|
||||||
|
grid-column-start: 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-control-button {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
--control-button-border-radius: 28px;
|
||||||
|
--mdc-icon-size: 24px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.submit {
|
||||||
|
--control-button-background-color: var(--green-color);
|
||||||
|
--control-button-icon-color: var(--green-color);
|
||||||
|
}
|
||||||
|
.clear {
|
||||||
|
--control-button-background-color: var(--red-color);
|
||||||
|
--control-button-icon-color: var(--red-color);
|
||||||
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"dialog-enter-code": DialogEnterCode;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,216 @@
|
|||||||
|
import {
|
||||||
|
mdiAirplane,
|
||||||
|
mdiHome,
|
||||||
|
mdiLock,
|
||||||
|
mdiMoonWaningCrescent,
|
||||||
|
mdiShield,
|
||||||
|
mdiShieldOff,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { computeAttributeNameDisplay } from "../../../../common/entity/compute_attribute_display";
|
||||||
|
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||||
|
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||||
|
import "../../../../components/ha-control-select";
|
||||||
|
import type { ControlSelectOption } from "../../../../components/ha-control-select";
|
||||||
|
import "../../../../components/ha-control-slider";
|
||||||
|
import {
|
||||||
|
AlarmControlPanelEntity,
|
||||||
|
AlarmControlPanelEntityFeature,
|
||||||
|
} from "../../../../data/alarm_control_panel";
|
||||||
|
import { HomeAssistant } from "../../../../types";
|
||||||
|
import { showEnterCodeDialogDialog } from "./show-enter-code-dialog";
|
||||||
|
|
||||||
|
type AlarmMode =
|
||||||
|
| "away"
|
||||||
|
| "home"
|
||||||
|
| "night"
|
||||||
|
| "vacation"
|
||||||
|
| "custom_bypass"
|
||||||
|
| "disarmed";
|
||||||
|
|
||||||
|
type AlarmConfig = {
|
||||||
|
service: string;
|
||||||
|
feature?: AlarmControlPanelEntityFeature;
|
||||||
|
state: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
const ALARM_MODES: Record<AlarmMode, AlarmConfig> = {
|
||||||
|
away: {
|
||||||
|
feature: AlarmControlPanelEntityFeature.ARM_AWAY,
|
||||||
|
service: "alarm_arm_away",
|
||||||
|
state: "armed_away",
|
||||||
|
path: mdiLock,
|
||||||
|
},
|
||||||
|
home: {
|
||||||
|
feature: AlarmControlPanelEntityFeature.ARM_HOME,
|
||||||
|
service: "alarm_arm_home",
|
||||||
|
state: "armed_home",
|
||||||
|
path: mdiHome,
|
||||||
|
},
|
||||||
|
custom_bypass: {
|
||||||
|
feature: AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS,
|
||||||
|
service: "alarm_arm_custom_bypass",
|
||||||
|
state: "armed_custom_bypass",
|
||||||
|
path: mdiShield,
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
feature: AlarmControlPanelEntityFeature.ARM_NIGHT,
|
||||||
|
service: "alarm_arm_night",
|
||||||
|
state: "armed_night",
|
||||||
|
path: mdiMoonWaningCrescent,
|
||||||
|
},
|
||||||
|
vacation: {
|
||||||
|
feature: AlarmControlPanelEntityFeature.ARM_VACATION,
|
||||||
|
service: "alarm_arm_vacation",
|
||||||
|
state: "armed_vacation",
|
||||||
|
path: mdiAirplane,
|
||||||
|
},
|
||||||
|
disarmed: {
|
||||||
|
service: "alarm_disarm",
|
||||||
|
state: "disarmed",
|
||||||
|
path: mdiShieldOff,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("ha-more-info-alarm_control_panel-modes")
|
||||||
|
export class HaMoreInfoAlarmControlPanelModes extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public stateObj!: AlarmControlPanelEntity;
|
||||||
|
|
||||||
|
@state() _currentMode?: AlarmMode;
|
||||||
|
|
||||||
|
private _modes = memoizeOne((stateObj: AlarmControlPanelEntity) => {
|
||||||
|
const modes = Object.keys(ALARM_MODES) as AlarmMode[];
|
||||||
|
return modes.filter((mode) => {
|
||||||
|
const feature = ALARM_MODES[mode as AlarmMode].feature;
|
||||||
|
return !feature || supportsFeature(stateObj, feature);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
protected updated(changedProp: Map<string | number | symbol, unknown>): void {
|
||||||
|
super.updated(changedProp);
|
||||||
|
if (changedProp.has("stateObj") && this.stateObj) {
|
||||||
|
const oldStateObj = changedProp.get("stateObj") as HassEntity | undefined;
|
||||||
|
|
||||||
|
if (!oldStateObj || this.stateObj.state !== oldStateObj.state) {
|
||||||
|
this._currentMode = this._getCurrentMode(this.stateObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getCurrentMode(stateObj: AlarmControlPanelEntity) {
|
||||||
|
return this._modes(stateObj).find(
|
||||||
|
(mode) => ALARM_MODES[mode].state === stateObj.state
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _valueChanged(ev: CustomEvent) {
|
||||||
|
const mode = (ev.detail as any).value as AlarmMode;
|
||||||
|
|
||||||
|
this._currentMode = mode;
|
||||||
|
|
||||||
|
const { state: modeState, service } = ALARM_MODES[mode];
|
||||||
|
|
||||||
|
if (modeState === this.stateObj.state) return;
|
||||||
|
|
||||||
|
let code: string | undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(mode !== "disarmed" &&
|
||||||
|
this.stateObj.attributes.code_arm_required &&
|
||||||
|
this.stateObj.attributes.code_format) ||
|
||||||
|
(mode === "disarmed" && this.stateObj.attributes.code_format)
|
||||||
|
) {
|
||||||
|
const disarm = mode === "disarmed";
|
||||||
|
|
||||||
|
const response = await showEnterCodeDialogDialog(this, {
|
||||||
|
codeFormat: this.stateObj.attributes.code_format,
|
||||||
|
title: this.hass.localize(
|
||||||
|
`ui.dialogs.more_info_control.alarm_control_panel.${
|
||||||
|
disarm ? "disarm_title" : "arm_title"
|
||||||
|
}`
|
||||||
|
),
|
||||||
|
submitText: this.hass.localize(
|
||||||
|
`ui.dialogs.more_info_control.alarm_control_panel.${
|
||||||
|
disarm ? "disarm_action" : "arm_action"
|
||||||
|
}`
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (!response) {
|
||||||
|
this._currentMode = this._getCurrentMode(this.stateObj);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
code = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.hass.callService("alarm_control_panel", service, {
|
||||||
|
entity_id: this.stateObj!.entity_id,
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
} catch (_err) {
|
||||||
|
this._currentMode = this._getCurrentMode(this.stateObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const color = stateColorCss(this.stateObj);
|
||||||
|
|
||||||
|
const modes = this._modes(this.stateObj);
|
||||||
|
|
||||||
|
const options = modes.map<ControlSelectOption>((mode) => ({
|
||||||
|
value: mode,
|
||||||
|
label: this.hass.localize(
|
||||||
|
`ui.dialogs.more_info_control.alarm_control_panel.modes.${mode}`
|
||||||
|
),
|
||||||
|
path: ALARM_MODES[mode].path,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-control-select
|
||||||
|
vertical
|
||||||
|
.options=${options}
|
||||||
|
.value=${this._currentMode}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
no-optimistic-update
|
||||||
|
.ariaLabel=${computeAttributeNameDisplay(
|
||||||
|
this.hass.localize,
|
||||||
|
this.stateObj,
|
||||||
|
this.hass.entities,
|
||||||
|
"percentage"
|
||||||
|
)}
|
||||||
|
style=${styleMap({
|
||||||
|
"--control-select-color": color,
|
||||||
|
"--modes-count": modes.length.toString(),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
</ha-control-select>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
ha-control-select {
|
||||||
|
height: 45vh;
|
||||||
|
max-height: max(320px, var(--modes-count, 1) * 80px);
|
||||||
|
min-height: max(200px, var(--modes-count, 1) * 80px);
|
||||||
|
--control-select-thickness: 100px;
|
||||||
|
--control-select-border-radius: 24px;
|
||||||
|
--control-select-color: var(--primary-color);
|
||||||
|
--control-select-background: var(--disabled-color);
|
||||||
|
--control-select-background-opacity: 0.2;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-more-info-alarm_control_panel-modes": HaMoreInfoAlarmControlPanelModes;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
|
||||||
|
export interface EnterCodeDialogParams {
|
||||||
|
codeFormat: "text" | "number";
|
||||||
|
submitText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
title?: string;
|
||||||
|
submit?: (code?: string) => void;
|
||||||
|
cancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showEnterCodeDialogDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
dialogParams: EnterCodeDialogParams
|
||||||
|
) =>
|
||||||
|
new Promise<string | null>((resolve) => {
|
||||||
|
const origCancel = dialogParams.cancel;
|
||||||
|
const origSubmit = dialogParams.submit;
|
||||||
|
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "dialog-enter-code",
|
||||||
|
dialogImport: () => import("./dialog-enter-code"),
|
||||||
|
dialogParams: {
|
||||||
|
...dialogParams,
|
||||||
|
cancel: () => {
|
||||||
|
resolve(null);
|
||||||
|
if (origCancel) {
|
||||||
|
origCancel();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submit: (code: string) => {
|
||||||
|
resolve(code);
|
||||||
|
if (origSubmit) {
|
||||||
|
origSubmit(code);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
@ -17,6 +17,7 @@ export const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
|
|||||||
export const EDITABLE_DOMAINS_WITH_UNIQUE_ID = ["script"];
|
export const EDITABLE_DOMAINS_WITH_UNIQUE_ID = ["script"];
|
||||||
/** Domains with with new more info design. */
|
/** Domains with with new more info design. */
|
||||||
export const DOMAINS_WITH_NEW_MORE_INFO = [
|
export const DOMAINS_WITH_NEW_MORE_INFO = [
|
||||||
|
"alarm_control_panel",
|
||||||
"cover",
|
"cover",
|
||||||
"fan",
|
"fan",
|
||||||
"input_boolean",
|
"input_boolean",
|
||||||
|
@ -1,70 +1,46 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/web/button/outlined-button";
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import { mdiShieldOff } from "@mdi/js";
|
||||||
import { css, html, LitElement, PropertyValues, nothing } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
import { domainIcon } from "../../../common/entity/domain_icon";
|
||||||
import "../../../components/ha-textfield";
|
import { stateColorCss } from "../../../common/entity/state_color";
|
||||||
import type { HaTextField } from "../../../components/ha-textfield";
|
import { AlarmControlPanelEntity } from "../../../data/alarm_control_panel";
|
||||||
import {
|
|
||||||
AlarmControlPanelEntityFeature,
|
|
||||||
callAlarmAction,
|
|
||||||
FORMAT_NUMBER,
|
|
||||||
} from "../../../data/alarm_control_panel";
|
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import "../components/alarm_control_panel/ha-more-info-alarm_control_panel-modes";
|
||||||
const BUTTONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "clear"];
|
import { showEnterCodeDialogDialog } from "../components/alarm_control_panel/show-enter-code-dialog";
|
||||||
const DISARM_ACTIONS = ["disarm"];
|
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
|
||||||
|
import "../components/ha-more-info-state-header";
|
||||||
|
|
||||||
@customElement("more-info-alarm_control_panel")
|
@customElement("more-info-alarm_control_panel")
|
||||||
export class MoreInfoAlarmControlPanel extends LitElement {
|
class MoreInfoAlarmControlPanel extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
@property({ attribute: false }) public stateObj?: AlarmControlPanelEntity;
|
||||||
|
|
||||||
@state() private _armActions: string[] = [];
|
private async _disarm() {
|
||||||
|
let code: string | undefined;
|
||||||
|
|
||||||
@query("#alarmCode") private _input?: HaTextField;
|
if (this.stateObj!.attributes.code_format) {
|
||||||
|
const response = await showEnterCodeDialogDialog(this, {
|
||||||
public willUpdate(changedProps: PropertyValues<this>) {
|
codeFormat: this.stateObj!.attributes.code_format,
|
||||||
super.willUpdate(changedProps);
|
title: this.hass.localize(
|
||||||
|
"ui.dialogs.more_info_control.alarm_control_panel.disarm_title"
|
||||||
if (!this.stateObj || !changedProps.has("stateObj")) {
|
),
|
||||||
return;
|
submitText: this.hass.localize(
|
||||||
|
"ui.dialogs.more_info_control.alarm_control_panel.disarm_action"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
code = response;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._armActions = [];
|
this.hass.callService("alarm_control_panel", "alarm_disarm", {
|
||||||
if (
|
entity_id: this.stateObj!.entity_id,
|
||||||
supportsFeature(this.stateObj, AlarmControlPanelEntityFeature.ARM_HOME)
|
code,
|
||||||
) {
|
});
|
||||||
this._armActions.push("arm_home");
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
supportsFeature(this.stateObj, AlarmControlPanelEntityFeature.ARM_AWAY)
|
|
||||||
) {
|
|
||||||
this._armActions.push("arm_away");
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
supportsFeature(this.stateObj, AlarmControlPanelEntityFeature.ARM_NIGHT)
|
|
||||||
) {
|
|
||||||
this._armActions.push("arm_night");
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
supportsFeature(
|
|
||||||
this.stateObj,
|
|
||||||
AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this._armActions.push("arm_custom_bypass");
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
supportsFeature(
|
|
||||||
this.stateObj,
|
|
||||||
AlarmControlPanelEntityFeature.ARM_VACATION
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this._armActions.push("arm_vacation");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@ -72,134 +48,104 @@ export class MoreInfoAlarmControlPanel extends LitElement {
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const color = stateColorCss(this.stateObj);
|
||||||
|
const style = {
|
||||||
|
"--icon-color": color,
|
||||||
|
};
|
||||||
return html`
|
return html`
|
||||||
${!this.stateObj.attributes.code_format
|
<ha-more-info-state-header
|
||||||
? ""
|
.hass=${this.hass}
|
||||||
: html`
|
.stateObj=${this.stateObj}
|
||||||
<div class="center">
|
></ha-more-info-state-header>
|
||||||
<ha-textfield
|
<div class="controls" style=${styleMap(style)}>
|
||||||
id="alarmCode"
|
${["triggered", "arming", "pending"].includes(this.stateObj.state)
|
||||||
.label=${this.hass.localize("ui.card.alarm_control_panel.code")}
|
? html`
|
||||||
type="password"
|
<div class="status">
|
||||||
.inputMode=${this.stateObj.attributes.code_format ===
|
<span></span>
|
||||||
FORMAT_NUMBER
|
<div class="icon">
|
||||||
? "numeric"
|
<ha-svg-icon
|
||||||
: "text"}
|
.path=${domainIcon("alarm_control_panel", this.stateObj)}
|
||||||
></ha-textfield>
|
></ha-svg-icon>
|
||||||
</div>
|
</div>
|
||||||
`}
|
<md-outlined-button
|
||||||
${this.stateObj.attributes.code_format !== FORMAT_NUMBER
|
.label=${this.hass.localize(
|
||||||
? ""
|
"ui.dialogs.more_info_control.alarm_control_panel.disarm_action"
|
||||||
: html`
|
)}
|
||||||
<div id="keypad">
|
@click=${this._disarm}
|
||||||
${BUTTONS.map((value) =>
|
>
|
||||||
value === ""
|
<ha-svg-icon slot="icon" .path=${mdiShieldOff}></ha-svg-icon>
|
||||||
? html`<mwc-button disabled></mwc-button>`
|
</md-outlined-button>
|
||||||
: html`
|
</div>
|
||||||
<mwc-button
|
`
|
||||||
.value=${value}
|
: html`
|
||||||
@click=${this._handlePadClick}
|
<ha-more-info-alarm_control_panel-modes
|
||||||
outlined
|
.stateObj=${this.stateObj}
|
||||||
class=${classMap({
|
.hass=${this.hass}
|
||||||
numberkey: value !== "clear",
|
>
|
||||||
})}
|
</ha-more-info-alarm_control_panel-modes>
|
||||||
>
|
`}
|
||||||
${value === "clear"
|
|
||||||
? this.hass!.localize(
|
|
||||||
`ui.card.alarm_control_panel.clear_code`
|
|
||||||
)
|
|
||||||
: value}
|
|
||||||
</mwc-button>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
<div class="actions">
|
|
||||||
${(this.stateObj.state === "disarmed"
|
|
||||||
? this._armActions
|
|
||||||
: DISARM_ACTIONS
|
|
||||||
).map(
|
|
||||||
(stateAction) => html`
|
|
||||||
<mwc-button
|
|
||||||
.action=${stateAction}
|
|
||||||
@click=${this._handleActionClick}
|
|
||||||
outlined
|
|
||||||
>
|
|
||||||
${this.hass!.localize(
|
|
||||||
`ui.card.alarm_control_panel.${stateAction}`
|
|
||||||
)}
|
|
||||||
</mwc-button>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<span></span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handlePadClick(e: MouseEvent): void {
|
static get styles(): CSSResultGroup {
|
||||||
const val = (e.currentTarget! as any).value;
|
return [
|
||||||
this._input!.value = val === "clear" ? "" : this._input!.value + val;
|
moreInfoControlStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
--icon-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
md-outlined-button {
|
||||||
|
--ha-icon-display: block;
|
||||||
|
--md-sys-color-primary: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
.status md-outlined-button {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleActionClick(e: MouseEvent): void {
|
|
||||||
const input = this._input;
|
|
||||||
callAlarmAction(
|
|
||||||
this.hass!,
|
|
||||||
this.stateObj!.entity_id,
|
|
||||||
(e.currentTarget! as any).action,
|
|
||||||
input?.value || undefined
|
|
||||||
);
|
|
||||||
if (input) {
|
|
||||||
input.value = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
|
||||||
ha-textfield {
|
|
||||||
display: block;
|
|
||||||
margin: 8px;
|
|
||||||
max-width: 150px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#keypad {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin: auto;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#keypad mwc-button {
|
|
||||||
padding: 8px;
|
|
||||||
width: 30%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions mwc-button {
|
|
||||||
margin: 0 4px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
mwc-button#disarm {
|
|
||||||
color: var(--error-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
mwc-button.numberkey {
|
|
||||||
--mdc-typography-button-font-size: var(--keypad-font-size, 0.875rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -342,7 +342,8 @@ export const haStyleDialog = css`
|
|||||||
--ha-dialog-border-radius: 0px;
|
--ha-dialog-border-radius: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mwc-button.warning {
|
mwc-button.warning,
|
||||||
|
ha-button.warning {
|
||||||
--mdc-theme-primary: var(--error-color);
|
--mdc-theme-primary: var(--error-color);
|
||||||
}
|
}
|
||||||
.error {
|
.error {
|
||||||
|
@ -924,6 +924,20 @@
|
|||||||
"medium": "Medium",
|
"medium": "Medium",
|
||||||
"high": "High"
|
"high": "High"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"alarm_control_panel": {
|
||||||
|
"modes": {
|
||||||
|
"away": "Away",
|
||||||
|
"home": "Home",
|
||||||
|
"night": "Night",
|
||||||
|
"vacation": "Vacation",
|
||||||
|
"custom_bypass": "Custom",
|
||||||
|
"disarmed": "Disarmed"
|
||||||
|
},
|
||||||
|
"disarm_title": "Disarm",
|
||||||
|
"disarm_action": "Disarm",
|
||||||
|
"arm_title": "Arm",
|
||||||
|
"arm_action": "Arm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity_registry": {
|
"entity_registry": {
|
||||||
@ -1283,6 +1297,10 @@
|
|||||||
"release_items": "This includes beta releases for:",
|
"release_items": "This includes beta releases for:",
|
||||||
"view_documentation": "View documentation",
|
"view_documentation": "View documentation",
|
||||||
"join": "Join"
|
"join": "Join"
|
||||||
|
},
|
||||||
|
"enter_code": {
|
||||||
|
"title": "Enter code",
|
||||||
|
"input_label": "Code"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duration": {
|
"duration": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user