diff --git a/src/data/fan.ts b/src/data/fan.ts index 67a4e376b3..e0dda2cea7 100644 --- a/src/data/fan.ts +++ b/src/data/fan.ts @@ -11,7 +11,7 @@ export const enum FanEntityFeature { } interface FanEntityAttributes extends HassEntityAttributeBase { - direction?: number; + direction?: string; oscillating?: boolean; percentage?: number; percentage_step?: number; diff --git a/src/dialogs/more-info/components/fan/ha-more-info-fan-speed.ts b/src/dialogs/more-info/components/fan/ha-more-info-fan-speed.ts new file mode 100644 index 0000000000..736bd56b58 --- /dev/null +++ b/src/dialogs/more-info/components/fan/ha-more-info-fan-speed.ts @@ -0,0 +1,217 @@ +import { + mdiFan, + mdiFanOff, + mdiFanSpeed1, + mdiFanSpeed2, + mdiFanSpeed3, +} 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 { computeAttributeNameDisplay } from "../../../../common/entity/compute_attribute_display"; +import { computeStateDisplay } from "../../../../common/entity/compute_state_display"; +import { stateColorCss } from "../../../../common/entity/state_color"; +import "../../../../components/ha-control-select"; +import type { ControlSelectOption } from "../../../../components/ha-control-select"; +import "../../../../components/ha-control-slider"; +import { UNAVAILABLE } from "../../../../data/entity"; +import { FanEntity } from "../../../../data/fan"; +import { HomeAssistant } from "../../../../types"; + +type Speed = "off" | "low" | "medium" | "high" | "on"; + +const SPEEDS: Partial> = { + 2: ["off", "on"], + 3: ["off", "low", "high"], + 4: ["off", "low", "medium", "high"], +}; + +function percentageToSpeed(stateObj: HassEntity, value: number): string { + const step = stateObj.attributes.percentage_step ?? 1; + const speedValue = Math.round(value / step); + const speedCount = Math.round(100 / step) + 1; + + const speeds = SPEEDS[speedCount]; + return speeds?.[speedValue] ?? "off"; +} + +function speedToPercentage(stateObj: HassEntity, speed: Speed): number { + const step = stateObj.attributes.percentage_step ?? 1; + const speedCount = Math.round(100 / step) + 1; + + const speeds = SPEEDS[speedCount]; + + if (!speeds) { + return 0; + } + + const speedValue = speeds.indexOf(speed); + if (speedValue === -1) { + return 0; + } + return Math.round(speedValue * step); +} + +const SPEED_ICON_NUMBER: string[] = [mdiFanSpeed1, mdiFanSpeed2, mdiFanSpeed3]; + +export function getFanSpeedCount(stateObj: HassEntity) { + const step = stateObj.attributes.percentage_step ?? 1; + const speedCount = Math.round(100 / step) + 1; + return speedCount; +} + +export const FAN_SPEED_COUNT_MAX_FOR_BUTTONS = 4; + +@customElement("ha-more-info-fan-speed") +export class HaMoreInfoFanSpeed extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: FanEntity; + + @state() value?: number; + + protected updated(changedProp: Map): void { + if (changedProp.has("stateObj")) { + this.value = + this.stateObj.attributes.percentage != null + ? Math.max(Math.round(this.stateObj.attributes.percentage), 1) + : undefined; + } + } + + private _speedValueChanged(ev: CustomEvent) { + const speed = (ev.detail as any).value as Speed; + + const percentage = speedToPercentage(this.stateObj, speed); + + this.hass.callService("fan", "set_percentage", { + entity_id: this.stateObj!.entity_id, + percentage: percentage, + }); + } + + private _valueChanged(ev: CustomEvent) { + const value = (ev.detail as any).value; + if (isNaN(value)) return; + + this.hass.callService("fan", "set_percentage", { + entity_id: this.stateObj!.entity_id, + percentage: value, + }); + } + + private _localizeSpeed(speed: Speed) { + if (speed === "on" || speed === "off") { + return computeStateDisplay( + this.hass.localize, + this.stateObj, + this.hass.locale, + this.hass.entities, + speed + ); + } + return ( + this.hass.localize(`ui.dialogs.more_info_control.fan.speed.${speed}`) || + speed + ); + } + + protected render() { + const color = stateColorCss(this.stateObj); + + const speedCount = getFanSpeedCount(this.stateObj); + + if (speedCount <= FAN_SPEED_COUNT_MAX_FOR_BUTTONS) { + const options = SPEEDS[speedCount]!.map( + (speed, index) => ({ + value: speed, + label: this._localizeSpeed(speed), + path: + speed === "on" + ? mdiFan + : speed === "off" + ? mdiFanOff + : SPEED_ICON_NUMBER[index - 1], + }) + ).reverse(); + + const speed = percentageToSpeed( + this.stateObj, + this.stateObj.attributes.percentage ?? 0 + ); + + return html` + + + `; + } + + return html` + + + `; + } + + static get styles(): CSSResultGroup { + return css` + ha-control-slider { + height: 45vh; + max-height: 320px; + min-height: 200px; + --control-slider-thickness: 100px; + --control-slider-border-radius: 24px; + --control-slider-color: var(--primary-color); + --control-slider-background: var(--disabled-color); + --control-slider-background-opacity: 0.2; + } + ha-control-select { + height: 45vh; + max-height: 320px; + min-height: 200px; + --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-fan-speed": HaMoreInfoFanSpeed; + } +} diff --git a/src/dialogs/more-info/components/ha-more-info-control-style.ts b/src/dialogs/more-info/components/ha-more-info-control-style.ts index 1c3fed35e2..81032af738 100644 --- a/src/dialogs/more-info/components/ha-more-info-control-style.ts +++ b/src/dialogs/more-info/components/ha-more-info-control-style.ts @@ -18,6 +18,17 @@ export const moreInfoControlStyle = css` margin-bottom: 24px; } + .buttons { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 12px; + } + + .buttons > * { + margin: 4px; + } + ha-attributes { width: 100%; } diff --git a/src/dialogs/more-info/const.ts b/src/dialogs/more-info/const.ts index d091e1b395..9454bc39aa 100644 --- a/src/dialogs/more-info/const.ts +++ b/src/dialogs/more-info/const.ts @@ -17,6 +17,7 @@ export const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"]; export const EDITABLE_DOMAINS_WITH_UNIQUE_ID = ["script"]; /** Domains with with new more info design. */ export const DOMAINS_WITH_NEW_MORE_INFO = [ + "fan", "input_boolean", "light", "siren", diff --git a/src/dialogs/more-info/controls/more-info-fan.js b/src/dialogs/more-info/controls/more-info-fan.js deleted file mode 100644 index 2ec3925e49..0000000000 --- a/src/dialogs/more-info/controls/more-info-fan.js +++ /dev/null @@ -1,238 +0,0 @@ -import "@material/mwc-list/mwc-list-item"; -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { attributeClassNames } from "../../../common/entity/attribute_class_names"; -import { supportsFeature } from "../../../common/entity/supports-feature"; -import "../../../components/ha-attributes"; -import "../../../components/ha-icon"; -import "../../../components/ha-icon-button"; -import "../../../components/ha-labeled-slider"; -import "../../../components/ha-select"; -import "../../../components/ha-switch"; -import { FanEntityFeature } from "../../../data/fan"; -import { EventsMixin } from "../../../mixins/events-mixin"; -import LocalizeMixin from "../../../mixins/localize-mixin"; - -/* - * @appliesMixin EventsMixin - */ -class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) { - static get template() { - return html` - - - -
-
- -
- -
- - - -
- -
-
-
[[localize('ui.card.fan.oscillate')]]
- - -
-
- -
-
-
[[localize('ui.card.fan.direction')]]
- - - - - - -
-
-
- - - `; - } - - static get properties() { - return { - hass: { - type: Object, - }, - - stateObj: { - type: Object, - observer: "stateObjChanged", - }, - - oscillationToggleChecked: { - type: Boolean, - }, - - percentageSliderValue: { - type: Number, - }, - }; - } - - stateObjChanged(newVal, oldVal) { - if (newVal) { - this.setProperties({ - oscillationToggleChecked: newVal.attributes.oscillating, - percentageSliderValue: newVal.attributes.percentage, - }); - } - - if (oldVal) { - setTimeout(() => { - this.fire("iron-resize"); - }, 500); - } - } - - computePercentageStepSize(stateObj) { - if (stateObj.attributes.percentage_step) { - return stateObj.attributes.percentage_step; - } - return 1; - } - - computeClassNames(stateObj) { - return ( - "more-info-fan " + - (supportsFeature(stateObj, FanEntityFeature.SET_SPEED) - ? "has-percentage " - : "") + - (stateObj.attributes.preset_modes && - stateObj.attributes.preset_modes.length - ? "has-preset_modes " - : "") + - attributeClassNames(stateObj, ["oscillating", "direction"]) - ); - } - - presetModeChanged(ev) { - const oldVal = this.stateObj.attributes.preset_mode; - const newVal = ev.target.value; - - if (!newVal || oldVal === newVal) return; - - this.hass.callService("fan", "set_preset_mode", { - entity_id: this.stateObj.entity_id, - preset_mode: newVal, - }); - } - - stopPropagation(ev) { - ev.stopPropagation(); - } - - percentageChanged(ev) { - const oldVal = parseInt(this.stateObj.attributes.percentage, 10); - const newVal = ev.target.value; - - if (isNaN(newVal) || oldVal === newVal) return; - - this.hass.callService("fan", "set_percentage", { - entity_id: this.stateObj.entity_id, - percentage: newVal, - }); - } - - oscillationToggleChanged(ev) { - const oldVal = this.stateObj.attributes.oscillating; - const newVal = ev.target.checked; - - if (oldVal === newVal) return; - - this.hass.callService("fan", "oscillate", { - entity_id: this.stateObj.entity_id, - oscillating: newVal, - }); - } - - onDirectionReverse() { - this.hass.callService("fan", "set_direction", { - entity_id: this.stateObj.entity_id, - direction: "reverse", - }); - } - - onDirectionForward() { - this.hass.callService("fan", "set_direction", { - entity_id: this.stateObj.entity_id, - direction: "forward", - }); - } - - computeIsRotatingReverse(stateObj) { - return stateObj.attributes.direction === "reverse"; - } - - computeIsRotatingForward(stateObj) { - return stateObj.attributes.direction === "forward"; - } -} - -customElements.define("more-info-fan", MoreInfoFan); diff --git a/src/dialogs/more-info/controls/more-info-fan.ts b/src/dialogs/more-info/controls/more-info-fan.ts new file mode 100644 index 0000000000..d9c787b8f3 --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-fan.ts @@ -0,0 +1,323 @@ +import "@material/web/button/outlined-button"; +import "@material/web/iconbutton/outlined-icon-button"; +import { + mdiAutorenew, + mdiAutorenewOff, + mdiCreation, + mdiFan, + mdiFanOff, + mdiPower, + mdiRotateLeft, + mdiRotateRight, +} from "@mdi/js"; +import { + css, + CSSResultGroup, + html, + LitElement, + nothing, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; +import { + computeAttributeNameDisplay, + computeAttributeValueDisplay, +} from "../../../common/entity/compute_attribute_display"; +import { supportsFeature } from "../../../common/entity/supports-feature"; +import { blankBeforePercent } from "../../../common/translations/blank_before_percent"; +import "../../../components/ha-attributes"; +import { UNAVAILABLE } from "../../../data/entity"; +import { FanEntity, FanEntityFeature } from "../../../data/fan"; +import { forwardHaptic } from "../../../data/haptics"; +import type { HomeAssistant } from "../../../types"; +import { + FAN_SPEED_COUNT_MAX_FOR_BUTTONS, + getFanSpeedCount, +} from "../components/fan/ha-more-info-fan-speed"; +import { moreInfoControlStyle } from "../components/ha-more-info-control-style"; +import "../components/ha-more-info-state-header"; +import "../components/ha-more-info-toggle"; + +@customElement("more-info-fan") +class MoreInfoFan extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj?: FanEntity; + + @state() public _presetMode?: string; + + @state() private _selectedPercentage?: number; + + private _percentageChanged(ev) { + const value = (ev.detail as any).value; + if (isNaN(value)) return; + this._selectedPercentage = value; + } + + private _toggle = () => { + const service = this.stateObj?.state === "on" ? "turn_off" : "turn_on"; + forwardHaptic("light"); + this.hass.callService("fan", service, { + entity_id: this.stateObj!.entity_id, + }); + }; + + _setReverseDirection() { + this.hass.callService("fan", "set_direction", { + entity_id: this.stateObj!.entity_id, + direction: "reverse", + }); + } + + _setForwardDirection() { + this.hass.callService("fan", "set_direction", { + entity_id: this.stateObj!.entity_id, + direction: "forward", + }); + } + + _toggleOscillate() { + const oscillating = this.stateObj!.attributes.oscillating; + this.hass.callService("fan", "oscillate", { + entity_id: this.stateObj!.entity_id, + oscillating: !oscillating, + }); + } + + _handlePresetMode(ev) { + ev.stopPropagation(); + ev.preventDefault(); + + const index = ev.detail.index; + const newVal = this.stateObj!.attributes.preset_modes![index]; + const oldVal = this._presetMode; + + if (!newVal || oldVal === newVal) return; + + this._presetMode = newVal; + this.hass.callService("fan", "set_preset_mode", { + entity_id: this.stateObj!.entity_id, + preset_mode: newVal, + }); + } + + protected updated(changedProps: PropertyValues): void { + if (changedProps.has("stateObj")) { + this._presetMode = this.stateObj?.attributes.preset_mode; + this._selectedPercentage = this.stateObj?.attributes.percentage + ? Math.round(this.stateObj.attributes.percentage) + : undefined; + } + } + + protected render(): TemplateResult | null { + if (!this.hass || !this.stateObj) { + return null; + } + + const supportsSpeed = supportsFeature( + this.stateObj, + FanEntityFeature.SET_SPEED + ); + + const supportsDirection = supportsFeature( + this.stateObj, + FanEntityFeature.DIRECTION + ); + const supportsOscillate = supportsFeature( + this.stateObj, + FanEntityFeature.OSCILLATE + ); + const supportsPresetMode = supportsFeature( + this.stateObj, + FanEntityFeature.PRESET_MODE + ); + + const supportSpeedPercentage = + supportsSpeed && + getFanSpeedCount(this.stateObj) > FAN_SPEED_COUNT_MAX_FOR_BUTTONS; + + const stateOverride = this._selectedPercentage + ? `${Math.round(this._selectedPercentage)}${blankBeforePercent( + this.hass!.locale + )}%` + : undefined; + + return html` + +
+ ${ + supportsSpeed + ? html` + + + ` + : html` + + ` + } + ${ + supportSpeedPercentage || supportsDirection || supportsOscillate + ? html`
+ ${supportSpeedPercentage + ? html` + + + + ` + : null} + ${supportsDirection + ? html` + + + + + + + ` + : nothing} + ${supportsOscillate + ? html` + + + + ` + : nothing} +
` + : nothing + } + ${ + supportsPresetMode && this.stateObj.attributes.preset_modes + ? html` + + + + + ${this.stateObj.attributes.preset_modes?.map( + (mode) => + html` + + ${computeAttributeValueDisplay( + this.hass.localize, + this.stateObj!, + this.hass.locale, + this.hass.entities, + "preset_mode", + mode + )} + + ` + )} + + ` + : nothing + } +
+ + + `; + } + + static get styles(): CSSResultGroup { + return [ + moreInfoControlStyle, + css` + md-outlined-button { + --ha-icon-display: block; + --md-sys-color-primary: var(--primary-text-color); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "more-info-fan": MoreInfoFan; + } +} diff --git a/src/dialogs/more-info/controls/more-info-light.ts b/src/dialogs/more-info/controls/more-info-light.ts index 79dfbe7a77..be479fe41e 100644 --- a/src/dialogs/more-info/controls/more-info-light.ts +++ b/src/dialogs/more-info/controls/more-info-light.ts @@ -266,16 +266,6 @@ class MoreInfoLight extends LitElement { return [ moreInfoControlStyle, css` - .buttons { - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 12px; - } - .buttons > * { - margin: 4px; - } - md-outlined-icon-button-toggle, md-outlined-icon-button { --ha-icon-display: block; diff --git a/src/translations/en.json b/src/translations/en.json index b02bf832e7..4c4d1bdf74 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -909,6 +909,17 @@ "color_temp": "Temperature" } } + }, + "fan": { + "set_forward_direction": "Set forward direction", + "set_reverse_direction": "Set reverse direction", + "turn_on_oscillating": "Turn on oscillating", + "turn_off_oscillating": "Turn off oscillating", + "speed": { + "low": "Low", + "medium": "Medium", + "high": "High" + } } }, "entity_registry": {