Add tile card feature for counter actions (#24340)

* Add tile card feature for counter actions

* Format

* Change icon

* Disable buttons when hit limit

* Change increment/decrement icons
This commit is contained in:
Jan-Philipp Benecke 2025-02-20 20:09:44 +01:00 committed by GitHub
parent 0bd7d27c57
commit a7a4194e09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 251 additions and 1 deletions

View File

@ -0,0 +1,134 @@
import { mdiRestore, mdiPlus, mdiMinus } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-control-select";
import { UNAVAILABLE } from "../../../data/entity";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import { COUNTER_ACTIONS, type CounterActionsCardFeatureConfig } from "./types";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-button";
export const supportsCounterActionsCardFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "counter";
};
interface CounterButton {
translationKey: string;
icon: string;
serviceName: string;
disabled: boolean;
}
export const COUNTER_ACTIONS_BUTTON: Record<
string,
(stateObj: HassEntity) => CounterButton
> = {
increment: (stateObj) => ({
translationKey: "increment",
icon: mdiPlus,
serviceName: "increment",
disabled: parseInt(stateObj.state) === stateObj.attributes.maximum,
}),
reset: () => ({
translationKey: "reset",
icon: mdiRestore,
serviceName: "reset",
disabled: false,
}),
decrement: (stateObj) => ({
translationKey: "decrement",
icon: mdiMinus,
serviceName: "decrement",
disabled: parseInt(stateObj.state) === stateObj.attributes.minimum,
}),
};
@customElement("hui-counter-actions-card-feature")
class HuiCounterActionsCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state() private _config?: CounterActionsCardFeatureConfig;
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
await import(
"../editor/config-elements/hui-counter-actions-card-feature-editor"
);
return document.createElement("hui-counter-actions-card-feature-editor");
}
static getStubConfig(): CounterActionsCardFeatureConfig {
return {
type: "counter-actions",
actions: COUNTER_ACTIONS.map((action) => action),
};
}
public setConfig(config: CounterActionsCardFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
protected render(): TemplateResult | null {
if (
!this._config ||
!this.hass ||
!this.stateObj ||
!supportsCounterActionsCardFeature(this.stateObj)
) {
return null;
}
return html`
<ha-control-button-group>
${this._config?.actions
?.filter((action) => COUNTER_ACTIONS.includes(action))
.map((action) => {
const button = COUNTER_ACTIONS_BUTTON[action](this.stateObj!);
return html`
<ha-control-button
.entry=${button}
.label=${this.hass!.localize(
// @ts-ignore
`ui.card.counter.actions.${button.translationKey}`
)}
@click=${this._onActionTap}
.disabled=${button.disabled ||
this.stateObj?.state === UNAVAILABLE}
>
<ha-svg-icon .path=${button.icon}></ha-svg-icon>
</ha-control-button>
`;
})}
</ha-control-button-group>
`;
}
private _onActionTap(ev): void {
ev.stopPropagation();
const entry = (ev.target! as any).entry as CounterButton;
this.hass!.callService("counter", entry.serviceName, {
entity_id: this.stateObj!.entity_id,
});
}
static styles = cardFeatureStyles;
}
declare global {
interface HTMLElementTagNameMap {
"hui-counter-actions-card-feature": HuiCounterActionsCardFeature;
}
}

View File

@ -83,6 +83,15 @@ export interface ClimatePresetModesCardFeatureConfig {
preset_modes?: string[];
}
export const COUNTER_ACTIONS = ["increment", "reset", "decrement"] as const;
export type CounterActions = (typeof COUNTER_ACTIONS)[number];
export interface CounterActionsCardFeatureConfig {
type: "counter-actions";
actions?: CounterActions[];
}
export interface SelectOptionsCardFeatureConfig {
type: "select-options";
options?: string[];
@ -156,6 +165,7 @@ export type LovelaceCardFeatureConfig =
| ClimateSwingHorizontalModesCardFeatureConfig
| ClimateHvacModesCardFeatureConfig
| ClimatePresetModesCardFeatureConfig
| CounterActionsCardFeatureConfig
| CoverOpenCloseCardFeatureConfig
| CoverPositionCardFeatureConfig
| CoverTiltPositionCardFeatureConfig

View File

@ -4,6 +4,7 @@ import "../card-features/hui-climate-swing-modes-card-feature";
import "../card-features/hui-climate-swing-horizontal-modes-card-feature";
import "../card-features/hui-climate-hvac-modes-card-feature";
import "../card-features/hui-climate-preset-modes-card-feature";
import "../card-features/hui-counter-actions-card-feature";
import "../card-features/hui-cover-open-close-card-feature";
import "../card-features/hui-cover-position-card-feature";
import "../card-features/hui-cover-tilt-card-feature";
@ -40,6 +41,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
"climate-swing-horizontal-modes",
"climate-hvac-modes",
"climate-preset-modes",
"counter-actions",
"cover-open-close",
"cover-position",
"cover-tilt-position",

View File

@ -24,6 +24,7 @@ import { supportsClimateHvacModesCardFeature } from "../../card-features/hui-cli
import { supportsClimatePresetModesCardFeature } from "../../card-features/hui-climate-preset-modes-card-feature";
import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature";
import { supportsClimateSwingHorizontalModesCardFeature } from "../../card-features/hui-climate-swing-horizontal-modes-card-feature";
import { supportsCounterActionsCardFeature } from "../../card-features/hui-counter-actions-card-feature";
import { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature";
import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover-position-card-feature";
import { supportsCoverTiltCardFeature } from "../../card-features/hui-cover-tilt-card-feature";
@ -59,6 +60,7 @@ const UI_FEATURE_TYPES = [
"climate-preset-modes",
"climate-swing-modes",
"climate-swing-horizontal-modes",
"counter-actions",
"cover-open-close",
"cover-position",
"cover-tilt-position",
@ -92,6 +94,7 @@ const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
"climate-preset-modes",
"climate-swing-modes",
"climate-swing-horizontal-modes",
"counter-actions",
"fan-preset-modes",
"humidifier-modes",
"lawn-mower-commands",
@ -113,6 +116,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
supportsClimateSwingHorizontalModesCardFeature,
"climate-hvac-modes": supportsClimateHvacModesCardFeature,
"climate-preset-modes": supportsClimatePresetModesCardFeature,
"counter-actions": supportsCounterActionsCardFeature,
"cover-open-close": supportsCoverOpenCloseCardFeature,
"cover-position": supportsCoverPositionCardFeature,
"cover-tilt-position": supportsCoverTiltPositionCardFeature,

View File

@ -0,0 +1,91 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import "../../../../components/ha-form/ha-form";
import type { HomeAssistant } from "../../../../types";
import {
COUNTER_ACTIONS,
type LovelaceCardFeatureContext,
type CounterActionsCardFeatureConfig,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
@customElement("hui-counter-actions-card-feature-editor")
export class HuiCounterActionsCardFeatureEditor
extends LitElement
implements LovelaceCardFeatureEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: CounterActionsCardFeatureConfig;
public setConfig(config: CounterActionsCardFeatureConfig): void {
this._config = config;
}
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{
name: "actions",
selector: {
select: {
multiple: true,
mode: "list",
reorder: true,
options: COUNTER_ACTIONS.map((action) => ({
value: action,
label: `${localize(
`ui.panel.lovelace.editor.features.types.counter-actions.actions.${action}`
)}`,
})),
},
},
},
] as const
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const schema = this._schema(this.hass.localize);
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
}
};
}
declare global {
interface HTMLElementTagNameMap {
"hui-counter-actions-card-feature-editor": HuiCounterActionsCardFeatureEditor;
}
}

View File

@ -7001,7 +7001,8 @@
"suggested_cards": "Suggested cards",
"other_cards": "Other cards",
"custom_cards": "Custom cards",
"features": "Features"
"features": "Features",
"actions": "Actions"
},
"heading": {
"name": "Heading",
@ -7320,6 +7321,14 @@
"customize_modes": "Customize preset modes",
"preset_modes": "Preset modes"
},
"counter-actions": {
"label": "Counter actions",
"actions": {
"increment": "Increment",
"decrement": "Decrement",
"reset": "Reset"
}
},
"fan-preset-modes": {
"label": "Fan preset modes",
"style": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style%]",