diff --git a/src/components/ha-control-select.ts b/src/components/ha-control-select.ts index 8a2085cfa1..0bc3ba2687 100644 --- a/src/components/ha-control-select.ts +++ b/src/components/ha-control-select.ts @@ -205,8 +205,11 @@ export class HaControlSelect extends LitElement { --control-select-background: var(--disabled-color); --control-select-background-opacity: 0.2; --control-select-thickness: 40px; - --control-select-border-radius: 12px; + --control-select-border-radius: 10px; --control-select-padding: 4px; + --control-select-button-border-radius: calc( + var(--control-select-border-radius) - var(--control-select-padding) + ); --mdc-icon-size: 20px; height: var(--control-select-thickness); width: 100%; @@ -263,9 +266,7 @@ export class HaControlSelect extends LitElement { display: flex; align-items: center; justify-content: center; - border-radius: calc( - var(--control-select-border-radius) - var(--control-select-padding) - ); + border-radius: var(--control-select-button-border-radius); overflow: hidden; color: var(--primary-text-color); /* For safari border-radius overflow */ diff --git a/src/components/tile/ha-tile-slider.ts b/src/components/tile/ha-tile-slider.ts deleted file mode 100644 index b79bd0ad2c..0000000000 --- a/src/components/tile/ha-tile-slider.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators"; -import { ifDefined } from "lit/directives/if-defined"; -import "../ha-control-slider"; - -@customElement("ha-tile-slider") -export class HaTileSlider extends LitElement { - @property({ type: Boolean }) - public disabled = false; - - @property() - public mode?: "start" | "end" | "cursor" = "start"; - - @property({ type: Boolean, attribute: "show-handle" }) - public showHandle = false; - - @property({ type: Number }) - public value?: number; - - @property({ type: Number }) - public step = 1; - - @property({ type: Number }) - public min = 0; - - @property({ type: Number }) - public max = 100; - - @property() public label?: string; - - protected render(): TemplateResult { - return html` - - - `; - } - - static get styles(): CSSResultGroup { - return css` - ha-control-slider { - --control-slider-color: var(--tile-slider-color, var(--primary-color)); - --control-slider-background: var( - --tile-slider-background, - var(--disabled-color) - ); - --control-slider-background-opacity: var( - --tile-slider-background-opacity, - 0.2 - ); - --control-slider-thickness: 40px; - --control-slider-border-radius: 10px; - } - `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ha-tile-slider": HaTileSlider; - } -} diff --git a/src/data/fan.ts b/src/data/fan.ts index e0dda2cea7..ca8e9e5a63 100644 --- a/src/data/fan.ts +++ b/src/data/fan.ts @@ -1,3 +1,10 @@ +import { + mdiFan, + mdiFanOff, + mdiFanSpeed1, + mdiFanSpeed2, + mdiFanSpeed3, +} from "@mdi/js"; import { HassEntityAttributeBase, HassEntityBase, @@ -22,3 +29,65 @@ interface FanEntityAttributes extends HassEntityAttributeBase { export interface FanEntity extends HassEntityBase { attributes: FanEntityAttributes; } + +export type FanSpeed = "off" | "low" | "medium" | "high" | "on"; + +export const FAN_SPEEDS: Partial> = { + 2: ["off", "on"], + 3: ["off", "low", "high"], + 4: ["off", "low", "medium", "high"], +}; + +export function fanPercentageToSpeed( + stateObj: FanEntity, + value: number +): FanSpeed { + const step = stateObj.attributes.percentage_step ?? 1; + const speedValue = Math.round(value / step); + const speedCount = Math.round(100 / step) + 1; + + const speeds = FAN_SPEEDS[speedCount]; + return speeds?.[speedValue] ?? "off"; +} + +export function fanSpeedToPercentage( + stateObj: FanEntity, + speed: FanSpeed +): number { + const step = stateObj.attributes.percentage_step ?? 1; + const speedCount = Math.round(100 / step) + 1; + + const speeds = FAN_SPEEDS[speedCount]; + + if (!speeds) { + return 0; + } + + const speedValue = speeds.indexOf(speed); + if (speedValue === -1) { + return 0; + } + return Math.round(speedValue * step); +} + +export function computeFanSpeedCount(stateObj: FanEntity): number { + const step = stateObj.attributes.percentage_step ?? 1; + const speedCount = Math.round(100 / step) + 1; + return speedCount; +} + +export function computeFanSpeedIcon( + stateObj: FanEntity, + speed: FanSpeed +): string { + const speedCount = computeFanSpeedCount(stateObj); + const speeds = FAN_SPEEDS[speedCount]; + const index = speeds?.indexOf(speed) ?? 1; + + return speed === "on" + ? mdiFan + : speed === "off" + ? mdiFanOff + : [mdiFanSpeed1, mdiFanSpeed2, mdiFanSpeed3][index - 1]; +} +export const FAN_SPEED_COUNT_MAX_FOR_BUTTONS = 4; 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 index 736bd56b58..54bc13b937 100644 --- 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 @@ -1,11 +1,3 @@ -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"; @@ -16,53 +8,18 @@ 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 { + computeFanSpeedCount, + computeFanSpeedIcon, + FanEntity, + fanPercentageToSpeed, + FanSpeed, + fanSpeedToPercentage, + FAN_SPEEDS, + FAN_SPEED_COUNT_MAX_FOR_BUTTONS, +} 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; @@ -81,9 +38,9 @@ export class HaMoreInfoFanSpeed extends LitElement { } private _speedValueChanged(ev: CustomEvent) { - const speed = (ev.detail as any).value as Speed; + const speed = (ev.detail as any).value as FanSpeed; - const percentage = speedToPercentage(this.stateObj, speed); + const percentage = fanSpeedToPercentage(this.stateObj, speed); this.hass.callService("fan", "set_percentage", { entity_id: this.stateObj!.entity_id, @@ -101,7 +58,7 @@ export class HaMoreInfoFanSpeed extends LitElement { }); } - private _localizeSpeed(speed: Speed) { + private _localizeSpeed(speed: FanSpeed) { if (speed === "on" || speed === "off") { return computeStateDisplay( this.hass.localize, @@ -120,23 +77,18 @@ export class HaMoreInfoFanSpeed extends LitElement { protected render() { const color = stateColorCss(this.stateObj); - const speedCount = getFanSpeedCount(this.stateObj); + const speedCount = computeFanSpeedCount(this.stateObj); if (speedCount <= FAN_SPEED_COUNT_MAX_FOR_BUTTONS) { - const options = SPEEDS[speedCount]!.map( - (speed, index) => ({ + const options = FAN_SPEEDS[speedCount]!.map( + (speed) => ({ value: speed, label: this._localizeSpeed(speed), - path: - speed === "on" - ? mdiFan - : speed === "off" - ? mdiFanOff - : SPEED_ICON_NUMBER[index - 1], + path: computeFanSpeedIcon(this.stateObj, speed), }) ).reverse(); - const speed = percentageToSpeed( + const speed = fanPercentageToSpeed( this.stateObj, this.stateObj.attributes.percentage ?? 0 ); diff --git a/src/dialogs/more-info/controls/more-info-fan.ts b/src/dialogs/more-info/controls/more-info-fan.ts index ce8b7bb709..b094d723bd 100644 --- a/src/dialogs/more-info/controls/more-info-fan.ts +++ b/src/dialogs/more-info/controls/more-info-fan.ts @@ -27,15 +27,17 @@ 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 { + computeFanSpeedCount, + FanEntity, + FanEntityFeature, + FAN_SPEED_COUNT_MAX_FOR_BUTTONS, +} from "../../../data/fan"; import { forwardHaptic } from "../../../data/haptics"; import { haOscillating } from "../../../data/icons/haOscillating"; import { haOscillatingOff } from "../../../data/icons/haOscillatingOff"; import type { HomeAssistant } from "../../../types"; -import { - FAN_SPEED_COUNT_MAX_FOR_BUTTONS, - getFanSpeedCount, -} from "../components/fan/ha-more-info-fan-speed"; +import "../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"; @@ -137,7 +139,7 @@ class MoreInfoFan extends LitElement { const supportSpeedPercentage = supportsSpeed && - getFanSpeedCount(this.stateObj) > FAN_SPEED_COUNT_MAX_FOR_BUTTONS; + computeFanSpeedCount(this.stateObj) > FAN_SPEED_COUNT_MAX_FOR_BUTTONS; const stateOverride = this._selectedPercentage ? `${Math.round(this._selectedPercentage)}${blankBeforePercent( diff --git a/src/panels/lovelace/create-element/create-tile-feature-element.ts b/src/panels/lovelace/create-element/create-tile-feature-element.ts index dd91e8488e..7651b70410 100644 --- a/src/panels/lovelace/create-element/create-tile-feature-element.ts +++ b/src/panels/lovelace/create-element/create-tile-feature-element.ts @@ -7,12 +7,14 @@ import "../tile-features/hui-cover-open-close-tile-feature"; import "../tile-features/hui-cover-tilt-tile-feature"; import "../tile-features/hui-light-brightness-tile-feature"; import "../tile-features/hui-vacuum-commands-tile-feature"; +import "../tile-features/hui-fan-speed-tile-feature"; const TYPES: Set = new Set([ "cover-open-close", "cover-tilt", "light-brightness", "vacuum-commands", + "fan-speed", ]); export const createTileFeatureElement = (config: LovelaceTileFeatureConfig) => diff --git a/src/panels/lovelace/editor/config-elements/hui-tile-card-features-editor.ts b/src/panels/lovelace/editor/config-elements/hui-tile-card-features-editor.ts index 798c634936..0b4563b4a1 100644 --- a/src/panels/lovelace/editor/config-elements/hui-tile-card-features-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-tile-card-features-editor.ts @@ -27,6 +27,7 @@ import { HomeAssistant } from "../../../../types"; import { getTileFeatureElementClass } from "../../create-element/create-tile-feature-element"; import { supportsCoverOpenCloseTileFeature } from "../../tile-features/hui-cover-open-close-tile-feature"; import { supportsCoverTiltTileFeature } from "../../tile-features/hui-cover-tilt-tile-feature"; +import { supportsFanSpeedTileFeature } from "../../tile-features/hui-fan-speed-tile-feature"; import { supportsLightBrightnessTileFeature } from "../../tile-features/hui-light-brightness-tile-feature"; import { supportsVacuumCommandTileFeature } from "../../tile-features/hui-vacuum-commands-tile-feature"; import { LovelaceTileFeatureConfig } from "../../tile-features/types"; @@ -39,6 +40,7 @@ const FEATURE_TYPES: FeatureType[] = [ "cover-tilt", "light-brightness", "vacuum-commands", + "fan-speed", ]; const EDITABLES_FEATURE_TYPES = new Set(["vacuum-commands"]); @@ -49,6 +51,7 @@ const SUPPORTS_FEATURE_TYPES: Record = "cover-tilt": supportsCoverTiltTileFeature, "light-brightness": supportsLightBrightnessTileFeature, "vacuum-commands": supportsVacuumCommandTileFeature, + "fan-speed": supportsFanSpeedTileFeature, }; const CUSTOM_FEATURE_ENTRIES: Record< diff --git a/src/panels/lovelace/tile-features/hui-fan-speed-tile-feature.ts b/src/panels/lovelace/tile-features/hui-fan-speed-tile-feature.ts new file mode 100644 index 0000000000..464d3cba22 --- /dev/null +++ b/src/panels/lovelace/tile-features/hui-fan-speed-tile-feature.ts @@ -0,0 +1,191 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { css, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display"; +import { computeDomain } from "../../../common/entity/compute_domain"; +import { computeStateDisplay } from "../../../common/entity/compute_state_display"; +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 { UNAVAILABLE } from "../../../data/entity"; +import { + computeFanSpeedCount, + computeFanSpeedIcon, + FanEntityFeature, + fanPercentageToSpeed, + FanSpeed, + fanSpeedToPercentage, + FAN_SPEEDS, + FAN_SPEED_COUNT_MAX_FOR_BUTTONS, +} from "../../../data/fan"; +import { HomeAssistant } from "../../../types"; +import { LovelaceTileFeature } from "../types"; +import { FanSpeedTileFeatureConfig } from "./types"; + +export const supportsFanSpeedTileFeature = (stateObj: HassEntity) => { + const domain = computeDomain(stateObj.entity_id); + return ( + domain === "fan" && supportsFeature(stateObj, FanEntityFeature.SET_SPEED) + ); +}; + +@customElement("hui-fan-speed-tile-feature") +class HuiFanSpeedTileFeature extends LitElement implements LovelaceTileFeature { + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public stateObj?: HassEntity; + + @state() private _config?: FanSpeedTileFeatureConfig; + + static getStubConfig(): FanSpeedTileFeatureConfig { + return { + type: "fan-speed", + }; + } + + public setConfig(config: FanSpeedTileFeatureConfig): void { + if (!config) { + throw new Error("Invalid configuration"); + } + this._config = config; + } + + private _localizeSpeed(speed: FanSpeed) { + 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(): TemplateResult | null { + if ( + !this._config || + !this.hass || + !this.stateObj || + !supportsFanSpeedTileFeature(this.stateObj) + ) { + return null; + } + + const speedCount = computeFanSpeedCount(this.stateObj); + + if (speedCount <= FAN_SPEED_COUNT_MAX_FOR_BUTTONS) { + const options = FAN_SPEEDS[speedCount]!.map( + (speed) => ({ + value: speed, + label: this._localizeSpeed(speed), + path: computeFanSpeedIcon(this.stateObj!, speed), + }) + ); + + const speed = fanPercentageToSpeed( + this.stateObj, + this.stateObj.attributes.percentage ?? 0 + ); + + return html` +
+ + +
+ `; + } + + const percentage = + this.stateObj.attributes.percentage != null + ? Math.max(Math.round(this.stateObj.attributes.percentage), 0) + : undefined; + + return html` +
+ +
+ `; + } + + private _speedValueChanged(ev: CustomEvent) { + const speed = (ev.detail as any).value as FanSpeed; + + const percentage = fanSpeedToPercentage(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, + }); + } + + static get styles() { + return css` + ha-control-slider { + --control-slider-color: var(--tile-color); + --control-slider-background: var(--tile-color); + --control-slider-background-opacity: 0.2; + --control-slider-thickness: 40px; + --control-slider-border-radius: 10px; + } + ha-control-select { + --control-select-color: var(--tile-color); + --control-select-background: var(--tile-color); + --control-select-background-opacity: 0.2; + --control-select-padding: 0; + --control-select-thickness: 40px; + --control-select-border-radius: 10px; + --control-select-button-border-radius: 10px; + } + .container { + padding: 0 12px 12px 12px; + width: auto; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-fan-speed-tile-feature": HuiFanSpeedTileFeature; + } +} diff --git a/src/panels/lovelace/tile-features/hui-light-brightness-tile-feature.ts b/src/panels/lovelace/tile-features/hui-light-brightness-tile-feature.ts index f73261a41c..3476cece85 100644 --- a/src/panels/lovelace/tile-features/hui-light-brightness-tile-feature.ts +++ b/src/panels/lovelace/tile-features/hui-light-brightness-tile-feature.ts @@ -3,7 +3,7 @@ import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { stateActive } from "../../../common/entity/state_active"; -import "../../../components/tile/ha-tile-slider"; +import "../../../components/ha-control-slider"; import { UNAVAILABLE } from "../../../data/entity"; import { lightSupportsBrightness } from "../../../data/light"; import { HomeAssistant } from "../../../types"; @@ -59,7 +59,7 @@ class HuiLightBrightnessTileFeature return html`
- + >
`; } @@ -84,10 +84,12 @@ class HuiLightBrightnessTileFeature static get styles() { return css` - ha-tile-slider { - --tile-slider-color: var(--tile-color); - --tile-slider-background: var(--tile-color); - --tile-slider-background-opacity: 0.2; + ha-control-slider { + --control-slider-color: var(--tile-color); + --control-slider-background: var(--tile-color); + --control-slider-background-opacity: 0.2; + --control-slider-thickness: 40px; + --control-slider-border-radius: 10px; } .container { padding: 0 12px 12px 12px; diff --git a/src/panels/lovelace/tile-features/types.ts b/src/panels/lovelace/tile-features/types.ts index b741503969..e6ddbb6c18 100644 --- a/src/panels/lovelace/tile-features/types.ts +++ b/src/panels/lovelace/tile-features/types.ts @@ -10,6 +10,10 @@ export interface LightBrightnessTileFeatureConfig { type: "light-brightness"; } +export interface FanSpeedTileFeatureConfig { + type: "fan-speed"; +} + export const VACUUM_COMMANDS = [ "start_pause", "stop", @@ -29,7 +33,8 @@ export type LovelaceTileFeatureConfig = | CoverOpenCloseTileFeatureConfig | CoverTiltTileFeatureConfig | LightBrightnessTileFeatureConfig - | VacuumCommandsTileFeatureConfig; + | VacuumCommandsTileFeatureConfig + | FanSpeedTileFeatureConfig; export type LovelaceTileFeatureContext = { entity_id?: string; diff --git a/src/translations/en.json b/src/translations/en.json index 198014cab4..c1bc0d2dd2 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4455,6 +4455,9 @@ "cover-tilt": { "label": "Cover tilt" }, + "fan-speed": { + "label": "Fan speed" + }, "light-brightness": { "label": "Light brightness" },