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"
},