mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 03:06:41 +00:00
Add fan new more info (#15843)
* Add more info fan * Change icon * Use backend translations * Fix computeAttributeValueDisplay * Clean code * Clean code * Fix some styles * Improve ha-select rounded style * Use button instead of select * Show fan speed percentage
This commit is contained in:
parent
cd2996734c
commit
45c153d374
@ -11,7 +11,7 @@ export const enum FanEntityFeature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FanEntityAttributes extends HassEntityAttributeBase {
|
interface FanEntityAttributes extends HassEntityAttributeBase {
|
||||||
direction?: number;
|
direction?: string;
|
||||||
oscillating?: boolean;
|
oscillating?: boolean;
|
||||||
percentage?: number;
|
percentage?: number;
|
||||||
percentage_step?: number;
|
percentage_step?: number;
|
||||||
|
217
src/dialogs/more-info/components/fan/ha-more-info-fan-speed.ts
Normal file
217
src/dialogs/more-info/components/fan/ha-more-info-fan-speed.ts
Normal file
@ -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<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;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public stateObj!: FanEntity;
|
||||||
|
|
||||||
|
@state() value?: number;
|
||||||
|
|
||||||
|
protected updated(changedProp: Map<string | number | symbol, unknown>): 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<ControlSelectOption>(
|
||||||
|
(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`
|
||||||
|
<ha-control-select
|
||||||
|
vertical
|
||||||
|
.options=${options}
|
||||||
|
.value=${speed}
|
||||||
|
@value-changed=${this._speedValueChanged}
|
||||||
|
.ariaLabel=${computeAttributeNameDisplay(
|
||||||
|
this.hass.localize,
|
||||||
|
this.stateObj,
|
||||||
|
this.hass.entities,
|
||||||
|
"percentage"
|
||||||
|
)}
|
||||||
|
style=${styleMap({
|
||||||
|
"--control-select-color": color,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
</ha-control-select>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-control-slider
|
||||||
|
vertical
|
||||||
|
.value=${this.value}
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
.step=${this.stateObj.attributes.percentage_step ?? 1}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
.ariaLabel=${computeAttributeNameDisplay(
|
||||||
|
this.hass.localize,
|
||||||
|
this.stateObj,
|
||||||
|
this.hass.entities,
|
||||||
|
"percentage"
|
||||||
|
)}
|
||||||
|
style=${styleMap({
|
||||||
|
"--control-slider-color": color,
|
||||||
|
})}
|
||||||
|
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||||
|
>
|
||||||
|
</ha-control-slider>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,17 @@ export const moreInfoControlStyle = css`
|
|||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons > * {
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
ha-attributes {
|
ha-attributes {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ export const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
|
|||||||
export const EDITABLE_DOMAINS_WITH_UNIQUE_ID = ["script"];
|
export const EDITABLE_DOMAINS_WITH_UNIQUE_ID = ["script"];
|
||||||
/** Domains with with new more info design. */
|
/** Domains with with new more info design. */
|
||||||
export const DOMAINS_WITH_NEW_MORE_INFO = [
|
export const DOMAINS_WITH_NEW_MORE_INFO = [
|
||||||
|
"fan",
|
||||||
"input_boolean",
|
"input_boolean",
|
||||||
"light",
|
"light",
|
||||||
"siren",
|
"siren",
|
||||||
|
@ -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`
|
|
||||||
<style include="iron-flex"></style>
|
|
||||||
<style>
|
|
||||||
.container-preset_modes,
|
|
||||||
.container-direction,
|
|
||||||
.container-percentage,
|
|
||||||
.container-oscillating {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.has-percentage .container-percentage,
|
|
||||||
.has-preset_modes .container-preset_modes,
|
|
||||||
.has-direction .container-direction,
|
|
||||||
.has-oscillating .container-oscillating {
|
|
||||||
display: block;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-select {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class$="[[computeClassNames(stateObj)]]">
|
|
||||||
<div class="container-percentage">
|
|
||||||
<ha-labeled-slider
|
|
||||||
caption="[[localize('ui.card.fan.speed')]]"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
step="[[computePercentageStepSize(stateObj)]]"
|
|
||||||
value="{{percentageSliderValue}}"
|
|
||||||
on-change="percentageChanged"
|
|
||||||
pin=""
|
|
||||||
extra=""
|
|
||||||
></ha-labeled-slider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container-preset_modes">
|
|
||||||
<ha-select
|
|
||||||
label="[[localize('ui.card.fan.preset_mode')]]"
|
|
||||||
value="[[stateObj.attributes.preset_mode]]"
|
|
||||||
on-selected="presetModeChanged"
|
|
||||||
fixedMenuPosition
|
|
||||||
naturalMenuWidth
|
|
||||||
on-closed="stopPropagation"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
is="dom-repeat"
|
|
||||||
items="[[stateObj.attributes.preset_modes]]"
|
|
||||||
>
|
|
||||||
<mwc-list-item value="[[item]]">[[item]]</mwc-list-item>
|
|
||||||
</template>
|
|
||||||
</ha-select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container-oscillating">
|
|
||||||
<div class="center horizontal layout single-row">
|
|
||||||
<div class="flex">[[localize('ui.card.fan.oscillate')]]</div>
|
|
||||||
<ha-switch
|
|
||||||
checked="[[oscillationToggleChecked]]"
|
|
||||||
on-change="oscillationToggleChanged"
|
|
||||||
>
|
|
||||||
</ha-switch>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container-direction">
|
|
||||||
<div class="direction">
|
|
||||||
<div>[[localize('ui.card.fan.direction')]]</div>
|
|
||||||
<ha-icon-button
|
|
||||||
on-click="onDirectionReverse"
|
|
||||||
title="[[localize('ui.card.fan.reverse')]]"
|
|
||||||
disabled="[[computeIsRotatingReverse(stateObj)]]"
|
|
||||||
>
|
|
||||||
<ha-icon icon="hass:rotate-left"></ha-icon>
|
|
||||||
</ha-icon-button>
|
|
||||||
<ha-icon-button
|
|
||||||
on-click="onDirectionForward"
|
|
||||||
title="[[localize('ui.card.fan.forward')]]"
|
|
||||||
disabled="[[computeIsRotatingForward(stateObj)]]"
|
|
||||||
>
|
|
||||||
<ha-icon icon="hass:rotate-right"></ha-icon>
|
|
||||||
</ha-icon-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ha-attributes
|
|
||||||
hass="[[hass]]"
|
|
||||||
state-obj="[[stateObj]]"
|
|
||||||
extra-filters="percentage_step,speed,preset_mode,preset_modes,speed_list,percentage,oscillating,direction"
|
|
||||||
></ha-attributes>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
323
src/dialogs/more-info/controls/more-info-fan.ts
Normal file
323
src/dialogs/more-info/controls/more-info-fan.ts
Normal file
@ -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`
|
||||||
|
<ha-more-info-state-header
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${this.stateObj}
|
||||||
|
.stateOverride=${stateOverride}
|
||||||
|
></ha-more-info-state-header>
|
||||||
|
<div class="controls">
|
||||||
|
${
|
||||||
|
supportsSpeed
|
||||||
|
? html`
|
||||||
|
<ha-more-info-fan-speed
|
||||||
|
.stateObj=${this.stateObj}
|
||||||
|
.hass=${this.hass}
|
||||||
|
@slider-moved=${this._percentageChanged}
|
||||||
|
>
|
||||||
|
</ha-more-info-fan-speed>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<ha-more-info-toggle
|
||||||
|
.stateObj=${this.stateObj}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.iconPathOn=${mdiFan}
|
||||||
|
.iconPathOff=${mdiFanOff}
|
||||||
|
></ha-more-info-toggle>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
${
|
||||||
|
supportSpeedPercentage || supportsDirection || supportsOscillate
|
||||||
|
? html`<div class="buttons">
|
||||||
|
${supportSpeedPercentage
|
||||||
|
? html`
|
||||||
|
<md-outlined-icon-button
|
||||||
|
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||||
|
@click=${this._toggle}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiPower}></ha-svg-icon>
|
||||||
|
</md-outlined-icon-button>
|
||||||
|
`
|
||||||
|
: null}
|
||||||
|
${supportsDirection
|
||||||
|
? html`
|
||||||
|
<md-outlined-icon-button
|
||||||
|
.disabled=${this.stateObj.state === UNAVAILABLE ||
|
||||||
|
this.stateObj.attributes.direction === "reverse"}
|
||||||
|
.title=${this.hass.localize(
|
||||||
|
"ui.dialogs.more_info_control.fan.set_reverse_direction"
|
||||||
|
)}
|
||||||
|
.ariaLabel=${this.hass.localize(
|
||||||
|
"ui.dialogs.more_info_control.fan.set_reverse_direction"
|
||||||
|
)}
|
||||||
|
@click=${this._setReverseDirection}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiRotateLeft}></ha-svg-icon>
|
||||||
|
</md-outlined-icon-button>
|
||||||
|
<md-outlined-icon-button
|
||||||
|
.disabled=${this.stateObj.state === UNAVAILABLE ||
|
||||||
|
this.stateObj.attributes.direction === "forward"}
|
||||||
|
.title=${this.hass.localize(
|
||||||
|
"ui.dialogs.more_info_control.fan.set_forward_direction"
|
||||||
|
)}
|
||||||
|
.ariaLabel=${this.hass.localize(
|
||||||
|
"ui.dialogs.more_info_control.fan.set_forward_direction"
|
||||||
|
)}
|
||||||
|
@click=${this._setForwardDirection}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiRotateRight}></ha-svg-icon>
|
||||||
|
</md-outlined-icon-button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${supportsOscillate
|
||||||
|
? html`
|
||||||
|
<md-outlined-icon-button
|
||||||
|
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||||
|
.title=${this.hass.localize(
|
||||||
|
`ui.dialogs.more_info_control.fan.${
|
||||||
|
this.stateObj.attributes.oscillating
|
||||||
|
? "turn_off_oscillating"
|
||||||
|
: "turn_on_oscillating"
|
||||||
|
}`
|
||||||
|
)}
|
||||||
|
.ariaLabel=${this.hass.localize(
|
||||||
|
`ui.dialogs.more_info_control.fan.${
|
||||||
|
this.stateObj.attributes.oscillating
|
||||||
|
? "turn_off_oscillating"
|
||||||
|
: "turn_on_oscillating"
|
||||||
|
}`
|
||||||
|
)}
|
||||||
|
@click=${this._toggleOscillate}
|
||||||
|
>
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${this.stateObj.attributes.oscillating
|
||||||
|
? mdiAutorenew
|
||||||
|
: mdiAutorenewOff}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</md-outlined-icon-button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</div> `
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
${
|
||||||
|
supportsPresetMode && this.stateObj.attributes.preset_modes
|
||||||
|
? html`
|
||||||
|
<ha-button-menu
|
||||||
|
corner="BOTTOM_START"
|
||||||
|
@action=${this._handlePresetMode}
|
||||||
|
@closed=${stopPropagation}
|
||||||
|
fixed
|
||||||
|
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||||
|
>
|
||||||
|
<md-outlined-button
|
||||||
|
slot="trigger"
|
||||||
|
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||||
|
.label=${this._presetMode ||
|
||||||
|
computeAttributeNameDisplay(
|
||||||
|
this.hass.localize,
|
||||||
|
this.stateObj,
|
||||||
|
this.hass.entities,
|
||||||
|
"preset_mode"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="icon"
|
||||||
|
path=${mdiCreation}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</md-outlined-button>
|
||||||
|
${this.stateObj.attributes.preset_modes?.map(
|
||||||
|
(mode) =>
|
||||||
|
html`
|
||||||
|
<ha-list-item
|
||||||
|
.value=${mode}
|
||||||
|
.activated=${this._presetMode === mode}
|
||||||
|
>
|
||||||
|
${computeAttributeValueDisplay(
|
||||||
|
this.hass.localize,
|
||||||
|
this.stateObj!,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.entities,
|
||||||
|
"preset_mode",
|
||||||
|
mode
|
||||||
|
)}
|
||||||
|
</ha-list-item>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-button-menu>
|
||||||
|
`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<ha-attributes
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${this.stateObj}
|
||||||
|
extra-filters="percentage_step,speed,preset_mode,preset_modes,speed_list,percentage,oscillating,direction"
|
||||||
|
></ha-attributes>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -266,16 +266,6 @@ class MoreInfoLight extends LitElement {
|
|||||||
return [
|
return [
|
||||||
moreInfoControlStyle,
|
moreInfoControlStyle,
|
||||||
css`
|
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-toggle,
|
||||||
md-outlined-icon-button {
|
md-outlined-icon-button {
|
||||||
--ha-icon-display: block;
|
--ha-icon-display: block;
|
||||||
|
@ -909,6 +909,17 @@
|
|||||||
"color_temp": "Temperature"
|
"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": {
|
"entity_registry": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user