Fan speed tile feature (#15958)

* Move fan speed rules outside fan more info

* Add fan speed tile feature

* Improve select style
This commit is contained in:
Paul Bottein 2023-03-28 17:59:07 +02:00 committed by GitHub
parent a6f9482bf6
commit f2cf598f98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 314 additions and 154 deletions

View File

@ -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 */

View File

@ -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`
<ha-control-slider
.disabled=${this.disabled}
.mode=${this.mode}
.value=${this.value}
.step=${this.step}
.min=${this.min}
.max=${this.max}
aria-label=${ifDefined(this.label)}
.showHandle=${this.showHandle}
>
</ha-control-slider>
`;
}
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;
}
}

View File

@ -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<Record<number, FanSpeed[]>> = {
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;

View File

@ -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<Record<number, Speed[]>> = {
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<ControlSelectOption>(
(speed, index) => ({
const options = FAN_SPEEDS[speedCount]!.map<ControlSelectOption>(
(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
);

View File

@ -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(

View File

@ -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<LovelaceTileFeatureConfig["type"]> = new Set([
"cover-open-close",
"cover-tilt",
"light-brightness",
"vacuum-commands",
"fan-speed",
]);
export const createTileFeatureElement = (config: LovelaceTileFeatureConfig) =>

View File

@ -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<FeatureType>(["vacuum-commands"]);
@ -49,6 +51,7 @@ const SUPPORTS_FEATURE_TYPES: Record<FeatureType, SupportsFeature | undefined> =
"cover-tilt": supportsCoverTiltTileFeature,
"light-brightness": supportsLightBrightnessTileFeature,
"vacuum-commands": supportsVacuumCommandTileFeature,
"fan-speed": supportsFanSpeedTileFeature,
};
const CUSTOM_FEATURE_ENTRIES: Record<

View File

@ -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<ControlSelectOption>(
(speed) => ({
value: speed,
label: this._localizeSpeed(speed),
path: computeFanSpeedIcon(this.stateObj!, speed),
})
);
const speed = fanPercentageToSpeed(
this.stateObj,
this.stateObj.attributes.percentage ?? 0
);
return html`
<div class="container">
<ha-control-select
.options=${options}
.value=${speed}
@value-changed=${this._speedValueChanged}
hide-label
.label=${computeAttributeNameDisplay(
this.hass.localize,
this.stateObj,
this.hass.entities,
"percentage"
)}
>
</ha-control-select>
</div>
`;
}
const percentage =
this.stateObj.attributes.percentage != null
? Math.max(Math.round(this.stateObj.attributes.percentage), 0)
: undefined;
return html`
<div class="container">
<ha-control-slider
.value=${percentage}
min="0"
max="100"
.step=${this.stateObj.attributes.percentage_step ?? 1}
.disabled=${this.stateObj!.state === UNAVAILABLE}
@value-changed=${this._valueChanged}
.label=${computeAttributeNameDisplay(
this.hass.localize,
this.stateObj,
this.hass.entities,
"percentage"
)}
></ha-control-slider>
</div>
`;
}
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;
}
}

View File

@ -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`
<div class="container">
<ha-tile-slider
<ha-control-slider
.value=${position}
min="1"
max="100"
@ -67,7 +67,7 @@ class HuiLightBrightnessTileFeature
.disabled=${this.stateObj!.state === UNAVAILABLE}
@value-changed=${this._valueChanged}
.label=${this.hass.localize("ui.card.light.brightness")}
></ha-tile-slider>
></ha-control-slider>
</div>
`;
}
@ -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;

View File

@ -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;

View File

@ -4455,6 +4455,9 @@
"cover-tilt": {
"label": "Cover tilt"
},
"fan-speed": {
"label": "Fan speed"
},
"light-brightness": {
"label": "Light brightness"
},