From 5f015ac9af34c5fbf73c582f50855cebb0e33792 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 21 Aug 2023 13:17:11 +0200 Subject: [PATCH] Add basic more info for lawn mower (#17601) * Add basic more info for lawn mower * Change buttons layout --- src/common/translations/localize.ts | 2 + src/components/ha-lawn_mower-state.ts | 91 ++++++++ src/data/lawn_mower.ts | 42 ++++ src/dialogs/more-info/const.ts | 1 + .../controls/more-info-lawn_mower.ts | 213 ++++++++++++++++++ .../more-info/state_more_info_control.ts | 1 + src/state-summary/state-card-content.js | 3 +- src/state-summary/state-card-lawn_mower.ts | 43 ++++ src/translations/en.json | 14 ++ 9 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 src/components/ha-lawn_mower-state.ts create mode 100644 src/data/lawn_mower.ts create mode 100644 src/dialogs/more-info/controls/more-info-lawn_mower.ts create mode 100644 src/state-summary/state-card-lawn_mower.ts diff --git a/src/common/translations/localize.ts b/src/common/translations/localize.ts index 15773a5cb5..c12f468423 100644 --- a/src/common/translations/localize.ts +++ b/src/common/translations/localize.ts @@ -11,10 +11,12 @@ export type LocalizeKeys = | `ui.card.alarm_control_panel.${string}` | `ui.card.weather.attributes.${string}` | `ui.card.weather.cardinal_direction.${string}` + | `ui.card.lawn_mower.actions.${string}` | `ui.components.calendar.event.rrule.${string}` | `ui.components.logbook.${string}` | `ui.components.selectors.file.${string}` | `ui.dialogs.entity_registry.editor.${string}` + | `ui.dialogs.more_info_control.lawn_mower.${string}` | `ui.dialogs.more_info_control.vacuum.${string}` | `ui.dialogs.quick-bar.commands.${string}` | `ui.dialogs.unhealthy.reason.${string}` diff --git a/src/components/ha-lawn_mower-state.ts b/src/components/ha-lawn_mower-state.ts new file mode 100644 index 0000000000..8214ae1ee0 --- /dev/null +++ b/src/components/ha-lawn_mower-state.ts @@ -0,0 +1,91 @@ +import "@material/mwc-button"; +import { CSSResultGroup, LitElement, css, html } from "lit"; +import { customElement, property } from "lit/decorators"; +import { supportsFeature } from "../common/entity/supports-feature"; +import { + LawnMowerEntity, + LawnMowerEntityFeature, + LawnMowerEntityState, +} from "../data/lawn_mower"; +import { HomeAssistant } from "../types"; + +type LawnMowerAction = { + action: string; + service: string; + feature: LawnMowerEntityFeature; +}; + +const LAWN_MOWER_ACTIONS: Partial< + Record +> = { + mowing: { + action: "dock", + service: "dock", + feature: LawnMowerEntityFeature.DOCK, + }, + docked: { + action: "start_mowing", + service: "start_mowing", + feature: LawnMowerEntityFeature.START_MOWING, + }, + paused: { + action: "resume_mowing", + service: "start_mowing", + feature: LawnMowerEntityFeature.START_MOWING, + }, +}; + +@customElement("ha-lawn_mower-state") +class HaLawnMowerState extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: LawnMowerEntity; + + public render() { + const state = this.stateObj.state; + const action = LAWN_MOWER_ACTIONS[state]; + + if (action && supportsFeature(this.stateObj, action.feature)) { + return html` + + ${this.hass.localize(`ui.card.lawn_mower.actions.${action.action}`)} + + `; + } + + return html` + + ${this.hass.formatEntityState(this.stateObj)} + + `; + } + + callService(ev) { + ev.stopPropagation(); + const stateObj = this.stateObj; + const service = ev.target.service; + this.hass.callService("lawn_mower", service, { + entity_id: stateObj.entity_id, + }); + } + + static get styles(): CSSResultGroup { + return css` + mwc-button { + top: 3px; + height: 37px; + margin-right: -0.57em; + } + mwc-button[disabled] { + background-color: transparent; + color: var(--secondary-text-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-lawn_mower-state": HaLawnMowerState; + } +} diff --git a/src/data/lawn_mower.ts b/src/data/lawn_mower.ts new file mode 100644 index 0000000000..2d06b557a8 --- /dev/null +++ b/src/data/lawn_mower.ts @@ -0,0 +1,42 @@ +import { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; +import { UNAVAILABLE } from "./entity"; + +export type LawnMowerEntityState = "paused" | "mowing" | "docked" | "error"; + +export const enum LawnMowerEntityFeature { + START_MOWING = 1, + PAUSE = 2, + DOCK = 4, +} + +interface LawnMowerEntityAttributes extends HassEntityAttributeBase { + [key: string]: any; +} + +export interface LawnMowerEntity extends HassEntityBase { + attributes: LawnMowerEntityAttributes; +} + +export function canStartMowing(stateObj: LawnMowerEntity): boolean { + if (stateObj.state === UNAVAILABLE) { + return false; + } + return stateObj.state !== "mowing"; +} + +export function canPause(stateObj: LawnMowerEntity): boolean { + if (stateObj.state === UNAVAILABLE) { + return false; + } + return stateObj.state !== "paused"; +} + +export function canDock(stateObj: LawnMowerEntity): boolean { + if (stateObj.state === UNAVAILABLE) { + return false; + } + return stateObj.state !== "docked"; +} diff --git a/src/dialogs/more-info/const.ts b/src/dialogs/more-info/const.ts index d77742b5be..80c5d5994f 100644 --- a/src/dialogs/more-info/const.ts +++ b/src/dialogs/more-info/const.ts @@ -46,6 +46,7 @@ export const DOMAINS_WITH_MORE_INFO = [ "image", "input_boolean", "input_datetime", + "lawn_mower", "light", "lock", "media_player", diff --git a/src/dialogs/more-info/controls/more-info-lawn_mower.ts b/src/dialogs/more-info/controls/more-info-lawn_mower.ts new file mode 100644 index 0000000000..dca0aa51a3 --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-lawn_mower.ts @@ -0,0 +1,213 @@ +import { mdiHomeMapMarker, mdiPause, mdiPlay } from "@mdi/js"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; +import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { computeStateDisplay } from "../../../common/entity/compute_state_display"; +import { computeStateDomain } from "../../../common/entity/compute_state_domain"; +import { supportsFeature } from "../../../common/entity/supports-feature"; +import { blankBeforePercent } from "../../../common/translations/blank_before_percent"; +import "../../../components/entity/ha-battery-icon"; +import "../../../components/ha-icon-button"; +import { UNAVAILABLE } from "../../../data/entity"; +import { + EntityRegistryDisplayEntry, + findBatteryChargingEntity, + findBatteryEntity, +} from "../../../data/entity_registry"; +import { + LawnMowerEntity, + LawnMowerEntityFeature, +} from "../../../data/lawn_mower"; +import { HomeAssistant } from "../../../types"; + +interface LawnMowerCommand { + translationKey: string; + icon: string; + serviceName: string; + isVisible: (stateObj: LawnMowerEntity) => boolean; +} + +const LAWN_MOWER_COMMANDS: LawnMowerCommand[] = [ + { + translationKey: "start_mowing", + icon: mdiPlay, + serviceName: "start_mowing", + isVisible: (stateObj) => + supportsFeature(stateObj, LawnMowerEntityFeature.START_MOWING), + }, + { + translationKey: "pause", + icon: mdiPause, + serviceName: "pause", + isVisible: (stateObj) => + supportsFeature(stateObj, LawnMowerEntityFeature.PAUSE), + }, + { + translationKey: "dock", + icon: mdiHomeMapMarker, + serviceName: "dock", + isVisible: (stateObj) => + supportsFeature(stateObj, LawnMowerEntityFeature.DOCK), + }, +]; + +@customElement("more-info-lawn_mower") +class MoreInfoLawnMower extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public stateObj?: LawnMowerEntity; + + protected render() { + if (!this.hass || !this.stateObj) { + return nothing; + } + + const stateObj = this.stateObj; + + return html` + ${stateObj.state !== UNAVAILABLE + ? html`
+
+ ${this.hass!.localize( + "ui.dialogs.more_info_control.lawn_mower.activity" + )}: + + + + ${computeStateDisplay( + this.hass.localize, + stateObj, + this.hass.locale, + this.hass.config, + this.hass.entities + )} + + +
+ ${this._renderBattery()} +
` + : nothing} + ${LAWN_MOWER_COMMANDS.some((item) => item.isVisible(stateObj)) + ? html` +
+

+
+ ${this.hass!.localize( + "ui.dialogs.more_info_control.lawn_mower.commands" + )} +
+
+ ${LAWN_MOWER_COMMANDS.filter((item) => + item.isVisible(stateObj) + ).map( + (item) => html` +
+ +
+ ` + )} +
+
+ ` + : ""} + `; + } + + private _deviceEntities = memoizeOne( + ( + deviceId: string, + entities: HomeAssistant["entities"] + ): EntityRegistryDisplayEntry[] => { + const entries = Object.values(entities); + return entries.filter((entity) => entity.device_id === deviceId); + } + ); + + private _renderBattery() { + const stateObj = this.stateObj!; + + const deviceId = this.hass.entities[stateObj.entity_id]?.device_id; + + const entities = deviceId + ? this._deviceEntities(deviceId, this.hass.entities) + : []; + + const batteryEntity = findBatteryEntity(this.hass, entities); + const battery = batteryEntity + ? this.hass.states[batteryEntity.entity_id] + : undefined; + + const batteryIsBinary = + battery && computeStateDomain(battery) === "binary_sensor"; + + // Use device battery entity + if (battery && (batteryIsBinary || !isNaN(battery.state as any))) { + const batteryChargingEntity = findBatteryChargingEntity( + this.hass, + entities + ); + const batteryCharging = batteryChargingEntity + ? this.hass.states[batteryChargingEntity?.entity_id] + : undefined; + + return html` +
+ + ${batteryIsBinary + ? "" + : `${Number(battery.state).toFixed()}${blankBeforePercent( + this.hass.locale + )}%`} + + +
+ `; + } + + return nothing; + } + + private callService(ev: CustomEvent) { + const entry = (ev.target! as any).entry as LawnMowerCommand; + this.hass.callService("lawn_mower", entry.serviceName, { + entity_id: this.stateObj!.entity_id, + }); + } + + static get styles(): CSSResultGroup { + return css` + :host { + line-height: 1.5; + } + .status-subtitle { + color: var(--secondary-text-color); + } + .flex-horizontal { + display: flex; + flex-direction: row; + } + .space-around { + justify-content: space-around; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "more-info-lawn_mower": MoreInfoLawnMower; + } +} diff --git a/src/dialogs/more-info/state_more_info_control.ts b/src/dialogs/more-info/state_more_info_control.ts index cc0d07eb67..65a2e0a1a0 100644 --- a/src/dialogs/more-info/state_more_info_control.ts +++ b/src/dialogs/more-info/state_more_info_control.ts @@ -21,6 +21,7 @@ const LAZY_LOADED_MORE_INFO_CONTROL = { image: () => import("./controls/more-info-image"), input_boolean: () => import("./controls/more-info-input_boolean"), input_datetime: () => import("./controls/more-info-input_datetime"), + lawn_mower: () => import("./controls/more-info-lawn_mower"), light: () => import("./controls/more-info-light"), lock: () => import("./controls/more-info-lock"), media_player: () => import("./controls/more-info-media_player"), diff --git a/src/state-summary/state-card-content.js b/src/state-summary/state-card-content.js index d62703f97c..c4d2e18206 100644 --- a/src/state-summary/state-card-content.js +++ b/src/state-summary/state-card-content.js @@ -5,15 +5,16 @@ import { stateCardType } from "../common/entity/state_card_type"; import "./state-card-alert"; import "./state-card-button"; import "./state-card-climate"; -import "./state-card-humidifier"; import "./state-card-configurator"; import "./state-card-cover"; import "./state-card-display"; import "./state-card-event"; +import "./state-card-humidifier"; import "./state-card-input_button"; import "./state-card-input_number"; import "./state-card-input_select"; import "./state-card-input_text"; +import "./state-card-lawn_mower"; import "./state-card-lock"; import "./state-card-media_player"; import "./state-card-number"; diff --git a/src/state-summary/state-card-lawn_mower.ts b/src/state-summary/state-card-lawn_mower.ts new file mode 100644 index 0000000000..afa8c570a2 --- /dev/null +++ b/src/state-summary/state-card-lawn_mower.ts @@ -0,0 +1,43 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { CSSResultGroup, LitElement, html } from "lit"; +import { customElement, property } from "lit/decorators"; +import "../components/entity/state-info"; +import "../components/ha-lawn_mower-state"; +import { haStyle } from "../resources/styles"; +import type { HomeAssistant } from "../types"; + +@customElement("state-card-lawn_mower") +class StateCardLawnMower extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public stateObj!: HassEntity; + + @property({ type: Boolean }) public inDialog = false; + + public render() { + const stateObj = this.stateObj; + return html` +
+ + +
+ `; + } + + static get styles(): CSSResultGroup { + return haStyle; + } +} + +declare global { + interface HTMLElementTagNameMap { + "state-card-lawn_mower": StateCardLawnMower; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index 448831c0a4..74bb2f1ac7 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -139,6 +139,13 @@ "drying": "{name} drying", "on_entity": "{name} on" }, + "lawn_mower": { + "actions": { + "resume_mowing": "Resume mowing", + "start_mowing": "Start mowing", + "dock": "Return to dock" + } + }, "light": { "brightness": "Brightness", "color_temperature": "Color temperature", @@ -1006,6 +1013,13 @@ "target_label": "[%key:ui::dialogs::more_info_control::climate::target_label%]", "target": "[%key:ui::dialogs::more_info_control::climate::target%]" }, + "lawn_mower": { + "activity": "Activity", + "commands": "Lawn mower commands:", + "start_mowing": "Start mowing", + "pause": "Pause", + "dock": "Return to dock" + }, "water_heater": { "target": "[%key:ui::dialogs::more_info_control::climate::target%]" }