diff --git a/package.json b/package.json index a9af1595db..bef9fe4631 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "@material/mwc-textfield": "^0.27.0", "@material/mwc-top-app-bar-fixed": "^0.27.0", "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0", + "@material/web": "=1.0.0-pre.2", "@mdi/js": "7.1.96", "@mdi/svg": "7.1.96", "@polymer/app-layout": "^3.1.0", diff --git a/src/components/ha-control-slider.ts b/src/components/ha-control-slider.ts index 34cdf0bb6b..2b761d4c30 100644 --- a/src/components/ha-control-slider.ts +++ b/src/components/ha-control-slider.ts @@ -44,7 +44,7 @@ const getPercentageFromEvent = (e: HammerInput, vertical: boolean) => { @customElement("ha-control-slider") export class HaControlSlider extends LitElement { - @property({ type: Boolean }) + @property({ type: Boolean, reflect: true }) public disabled = false; @property() @@ -245,14 +245,16 @@ export class HaControlSlider extends LitElement { >
${this.mode === "cursor" - ? html` -
- ` + ? this.value != null + ? html` +
+ ` + : null : html`
+ `; + } + + const stateDisplay = computeStateDisplay( + this.hass!.localize, + stateObj, + this.hass!.locale, + this.hass!.entities + ); + + return stateDisplay; + } + + protected render(): TemplateResult { + const name = this.stateObj.attributes.friendly_name; + + const stateDisplay = + this.stateOverride ?? this._computeStateDisplay(this.stateObj); + + return html` +

${name}

+

${stateDisplay}

+ `; + } + + static get styles(): CSSResultGroup { + return css` + p { + text-align: center; + margin: 0; + } + .name { + font-style: normal; + font-weight: 400; + font-size: 28px; + line-height: 36px; + margin-bottom: 4px; + } + .state { + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.1px; + margin-bottom: 24px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-more-info-state-header": HaMoreInfoStateHeader; + } +} diff --git a/src/dialogs/more-info/components/ha-more-info-toggle.ts b/src/dialogs/more-info/components/ha-more-info-toggle.ts new file mode 100644 index 0000000000..fabf68d407 --- /dev/null +++ b/src/dialogs/more-info/components/ha-more-info-toggle.ts @@ -0,0 +1,149 @@ +import { mdiFlash, mdiFlashOff } from "@mdi/js"; +import { HassEntity } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { styleMap } from "lit/directives/style-map"; +import { computeDomain } from "../../../common/entity/compute_domain"; +import { stateActive } from "../../../common/entity/state_active"; +import { stateColorCss } from "../../../common/entity/state_color"; +import "../../../components/ha-control-button"; +import "../../../components/ha-control-switch"; +import { UNAVAILABLE, UNKNOWN } from "../../../data/entity"; +import { HomeAssistant } from "../../../types"; + +@customElement("ha-more-info-toggle") +export class HaMoreInfoToggle extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: HassEntity; + + @property({ attribute: false }) public iconPathOn?: string; + + @property({ attribute: false }) public iconPathOff?: string; + + private _valueChanged(ev) { + const checked = ev.target.checked as boolean; + + if (checked) { + this._turnOn(); + } else { + this._turnOff(); + } + } + + private _turnOn() { + const domain = computeDomain(this.stateObj!.entity_id); + this.hass.callService(domain, "turn_on", { + entity_id: this.stateObj!.entity_id, + }); + } + + private _turnOff() { + const domain = computeDomain(this.stateObj!.entity_id); + this.hass.callService(domain, "turn_off", { + entity_id: this.stateObj!.entity_id, + }); + } + + protected render(): TemplateResult { + const color = stateColorCss(this.stateObj); + const isOn = this.stateObj.state === "on"; + const isOff = this.stateObj.state === "off"; + + if ( + this.stateObj.attributes.assumed_state || + this.stateObj.state === UNKNOWN + ) { + return html` +
+ + + + + + +
+ `; + } + + return html` + + + `; + } + + static get styles(): CSSResultGroup { + return css` + ha-control-switch { + height: 320px; + --control-switch-thickness: 100px; + --control-switch-border-radius: 24px; + --control-switch-padding: 6px; + --mdc-icon-size: 24px; + } + .buttons { + display: flex; + flex-direction: column; + width: 100px; + height: 320px; + padding: 6px; + box-sizing: border-box; + } + ha-control-button { + flex: 1; + width: 100%; + --button-bar-border-radius: 18px; + } + ha-control-button.active { + --button-bar-icon-color: white; + --button-bar-background-color: var(--color); + --button-bar-background-opacity: 1; + } + ha-control-button:not(:last-child) { + margin-bottom: 6px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-more-info-toggle": HaMoreInfoToggle; + } +} diff --git a/src/dialogs/more-info/components/lights/ha-more-info-light-brightness.ts b/src/dialogs/more-info/components/lights/ha-more-info-light-brightness.ts new file mode 100644 index 0000000000..b82226ec6f --- /dev/null +++ b/src/dialogs/more-info/components/lights/ha-more-info-light-brightness.ts @@ -0,0 +1,102 @@ +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import { + hsv2rgb, + rgb2hex, + rgb2hsv, +} from "../../../../common/color/convert-color"; +import { stateActive } from "../../../../common/entity/state_active"; +import { stateColorCss } from "../../../../common/entity/state_color"; +import "../../../../components/ha-control-slider"; +import { UNAVAILABLE } from "../../../../data/entity"; +import { LightEntity } from "../../../../data/light"; +import { HomeAssistant } from "../../../../types"; + +@customElement("ha-more-info-light-brightness") +export class HaMoreInfoLightBrightness extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public stateObj!: LightEntity; + + @state() value?: number; + + protected updated(changedProp: Map): void { + if (changedProp.has("stateObj")) { + this.value = + this.stateObj.attributes.brightness != null + ? Math.max( + Math.round((this.stateObj.attributes.brightness * 100) / 255), + 1 + ) + : undefined; + } + } + + private _valueChanged(ev: CustomEvent) { + const value = (ev.detail as any).value; + if (isNaN(value)) return; + + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + brightness_pct: value, + }); + } + + protected render(): TemplateResult { + let color = stateColorCss(this.stateObj); + + if (this.stateObj.attributes.rgb_color) { + const hsvColor = rgb2hsv(this.stateObj.attributes.rgb_color); + + // Modify the real rgb color for better contrast + if (hsvColor[1] < 0.4) { + // Special case for very light color (e.g: white) + if (hsvColor[1] < 0.1) { + hsvColor[2] = 225; + } else { + hsvColor[1] = 0.4; + } + } + color = rgb2hex(hsv2rgb(hsvColor)); + } + + return html` + + + `; + } + + static get styles(): CSSResultGroup { + return css` + ha-control-slider { + height: 320px; + --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; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-more-info-light-brightness": HaMoreInfoLightBrightness; + } +} diff --git a/src/dialogs/more-info/components/lights/ha-more-info-view-light-color-picker.ts b/src/dialogs/more-info/components/lights/ha-more-info-view-light-color-picker.ts new file mode 100644 index 0000000000..33058b76a7 --- /dev/null +++ b/src/dialogs/more-info/components/lights/ha-more-info-view-light-color-picker.ts @@ -0,0 +1,551 @@ +import "@material/mwc-button"; +import "@material/mwc-tab-bar/mwc-tab-bar"; +import "@material/mwc-tab/mwc-tab"; +import { mdiPalette } from "@mdi/js"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import "../../../../components/ha-button-toggle-group"; +import "../../../../components/ha-color-picker"; +import "../../../../components/ha-control-slider"; +import "../../../../components/ha-icon-button-prev"; +import "../../../../components/ha-labeled-slider"; +import { + getLightCurrentModeRgbColor, + LightColorMode, + LightEntity, + lightSupportsColor, + lightSupportsColorMode, +} from "../../../../data/light"; +import { haStyleDialog } from "../../../../resources/styles"; +import { HomeAssistant } from "../../../../types"; +import { LightColorPickerViewParams } from "./show-view-light-color-picker"; + +type Mode = "color_temp" | "color"; + +@customElement("ha-more-info-view-light-color-picker") +class MoreInfoViewLightColorPicker extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public params?: LightColorPickerViewParams; + + @state() private _ctSliderValue?: number; + + @state() private _cwSliderValue?: number; + + @state() private _wwSliderValue?: number; + + @state() private _wvSliderValue?: number; + + @state() private _colorBrightnessSliderValue?: number; + + @state() private _brightnessAdjusted?: number; + + @state() private _hueSegments = 24; + + @state() private _saturationSegments = 8; + + @state() private _colorPickerColor?: [number, number, number]; + + @state() private _mode?: Mode; + + @state() private _modes: Mode[] = []; + + get stateObj() { + return this.params + ? (this.hass.states[this.params.entityId] as LightEntity) + : undefined; + } + + protected render(): TemplateResult { + if (!this.params || !this.stateObj) { + return html``; + } + + const supportsRgbww = lightSupportsColorMode( + this.stateObj, + LightColorMode.RGBWW + ); + + const supportsRgbw = + !supportsRgbww && + lightSupportsColorMode(this.stateObj, LightColorMode.RGBW); + + return html` +
+ ${this._modes.length > 1 + ? html` + + ${this._modes.map( + (value) => + html`` + )} + + ` + : ""} +
+ ${this._mode === LightColorMode.COLOR_TEMP + ? html` + + + ` + : ""} + ${this._mode === "color" + ? html` +
+ + + +
+ + ${supportsRgbw || supportsRgbww + ? html`` + : ""} + ${supportsRgbw + ? html` + + ` + : ""} + ${supportsRgbww + ? html` + + + ` + : ""} + ` + : ""} +
+
+ `; + } + + public _updateSliderValues() { + const stateObj = this.stateObj; + + if (stateObj?.state === "on") { + this._brightnessAdjusted = undefined; + if ( + stateObj.attributes.color_mode === LightColorMode.RGB && + !lightSupportsColorMode(stateObj, LightColorMode.RGBWW) && + !lightSupportsColorMode(stateObj, LightColorMode.RGBW) + ) { + const maxVal = Math.max(...stateObj.attributes.rgb_color!); + + if (maxVal < 255) { + this._brightnessAdjusted = maxVal; + } + } + this._ctSliderValue = stateObj.attributes.color_temp_kelvin; + this._wvSliderValue = + stateObj.attributes.color_mode === LightColorMode.RGBW + ? Math.round((stateObj.attributes.rgbw_color![3] * 100) / 255) + : undefined; + this._cwSliderValue = + stateObj.attributes.color_mode === LightColorMode.RGBWW + ? Math.round((stateObj.attributes.rgbww_color![3] * 100) / 255) + : undefined; + this._wwSliderValue = + stateObj.attributes.color_mode === LightColorMode.RGBWW + ? Math.round((stateObj.attributes.rgbww_color![4] * 100) / 255) + : undefined; + + const currentRgbColor = getLightCurrentModeRgbColor(stateObj); + + this._colorBrightnessSliderValue = currentRgbColor + ? Math.round((Math.max(...currentRgbColor.slice(0, 3)) * 100) / 255) + : undefined; + + this._colorPickerColor = currentRgbColor?.slice(0, 3) as [ + number, + number, + number + ]; + } else { + this._colorPickerColor = [0, 0, 0]; + this._ctSliderValue = undefined; + this._wvSliderValue = undefined; + this._cwSliderValue = undefined; + this._wwSliderValue = undefined; + } + } + + public willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + + if (!changedProps.has("params") || !changedProps.has("hass")) { + return; + } + + if (changedProps.has("params")) { + const supportsTemp = lightSupportsColorMode( + this.stateObj!, + LightColorMode.COLOR_TEMP + ); + + const supportsColor = lightSupportsColor(this.stateObj!); + + const modes: Mode[] = []; + if (supportsColor) { + modes.push("color"); + } + if (supportsTemp) { + modes.push("color_temp"); + } + + this._modes = modes; + this._mode = + this.stateObj!.attributes.color_mode === LightColorMode.COLOR_TEMP + ? LightColorMode.COLOR_TEMP + : "color"; + } + + this._updateSliderValues(); + } + + private _handleTabChanged(ev: CustomEvent): void { + const newMode = this._modes[ev.detail.index]; + if (newMode === this._mode) { + return; + } + this._mode = newMode; + } + + private _ctSliderChanged(ev: CustomEvent) { + const ct = ev.detail.value; + + if (isNaN(ct)) { + return; + } + + this._ctSliderValue = ct; + + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + color_temp_kelvin: ct, + }); + } + + private _wvSliderChanged(ev: CustomEvent) { + const target = ev.target as any; + let wv = Number(target.value); + const name = target.name; + + if (isNaN(wv)) { + return; + } + + if (name === "wv") { + this._wvSliderValue = wv; + } else if (name === "cw") { + this._cwSliderValue = wv; + } else if (name === "ww") { + this._wwSliderValue = wv; + } + + wv = Math.min(255, Math.round((wv * 255) / 100)); + + const rgb = getLightCurrentModeRgbColor(this.stateObj!); + + if (name === "wv") { + const rgbw_color = rgb || [0, 0, 0, 0]; + rgbw_color[3] = wv; + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + rgbw_color, + }); + return; + } + + const rgbww_color = rgb || [0, 0, 0, 0, 0]; + while (rgbww_color.length < 5) { + rgbww_color.push(0); + } + rgbww_color[name === "cw" ? 3 : 4] = wv; + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + rgbww_color, + }); + } + + private _colorBrightnessSliderChanged(ev: CustomEvent) { + const target = ev.target as any; + let value = Number(target.value); + + if (isNaN(value)) { + return; + } + + const oldValue = this._colorBrightnessSliderValue; + this._colorBrightnessSliderValue = value; + + value = (value * 255) / 100; + + const rgb = (getLightCurrentModeRgbColor(this.stateObj!)?.slice(0, 3) || [ + 255, 255, 255, + ]) as [number, number, number]; + + this._setRgbWColor( + this._adjustColorBrightness( + // first normalize the value + oldValue + ? this._adjustColorBrightness(rgb, (oldValue * 255) / 100, true) + : rgb, + value + ) + ); + } + + private _segmentClick() { + if (this._hueSegments === 24 && this._saturationSegments === 8) { + this._hueSegments = 0; + this._saturationSegments = 0; + } else { + this._hueSegments = 24; + this._saturationSegments = 8; + } + } + + private _adjustColorBrightness( + rgbColor: [number, number, number], + value?: number, + invert = false + ) { + if (value !== undefined && value !== 255) { + let ratio = value / 255; + if (invert) { + ratio = 1 / ratio; + } + rgbColor[0] = Math.min(255, Math.round(rgbColor[0] * ratio)); + rgbColor[1] = Math.min(255, Math.round(rgbColor[1] * ratio)); + rgbColor[2] = Math.min(255, Math.round(rgbColor[2] * ratio)); + } + return rgbColor; + } + + private _setRgbWColor(rgbColor: [number, number, number]) { + if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW)) { + const rgbww_color: [number, number, number, number, number] = this + .stateObj!.attributes.rgbww_color + ? [...this.stateObj!.attributes.rgbww_color] + : [0, 0, 0, 0, 0]; + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + rgbww_color: rgbColor.concat(rgbww_color.slice(3)), + }); + } else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW)) { + const rgbw_color: [number, number, number, number] = this.stateObj! + .attributes.rgbw_color + ? [...this.stateObj!.attributes.rgbw_color] + : [0, 0, 0, 0]; + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + rgbw_color: rgbColor.concat(rgbw_color.slice(3)), + }); + } + } + + /** + * Called when a new color has been picked. + * should be throttled with the 'throttle=' attribute of the color picker + */ + private _colorPicked( + ev: CustomEvent<{ + hs: { h: number; s: number }; + rgb: { r: number; g: number; b: number }; + }> + ) { + this._colorPickerColor = [ + ev.detail.rgb.r, + ev.detail.rgb.g, + ev.detail.rgb.b, + ]; + + if ( + lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW) || + lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW) + ) { + this._setRgbWColor( + this._colorBrightnessSliderValue + ? this._adjustColorBrightness( + [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b], + (this._colorBrightnessSliderValue * 255) / 100 + ) + : [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b] + ); + } else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGB)) { + const rgb_color: [number, number, number] = [ + ev.detail.rgb.r, + ev.detail.rgb.g, + ev.detail.rgb.b, + ]; + if (this._brightnessAdjusted) { + const brightnessAdjust = (this._brightnessAdjusted / 255) * 100; + const brightnessPercentage = Math.round( + ((this.stateObj!.attributes.brightness || 0) * brightnessAdjust) / 255 + ); + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + brightness_pct: brightnessPercentage, + rgb_color: this._adjustColorBrightness( + rgb_color, + this._brightnessAdjusted, + true + ), + }); + } else { + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + rgb_color, + }); + } + } else { + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100], + }); + } + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + .content { + display: flex; + flex-direction: column; + align-items: center; + padding: 16px; + } + + .segmentationContainer { + position: relative; + max-height: 500px; + display: flex; + justify-content: center; + } + + .segmentationButton { + position: absolute; + top: 5%; + left: 0; + color: var(--secondary-text-color); + } + + ha-color-picker { + --ha-color-picker-wheel-borderwidth: 5; + --ha-color-picker-wheel-bordercolor: white; + --ha-color-picker-wheel-shadow: none; + --ha-color-picker-marker-borderwidth: 2; + --ha-color-picker-marker-bordercolor: white; + } + + ha-control-slider { + height: 320px; + margin: 20px 0; + } + + ha-labeled-slider { + width: 100%; + } + + .color_temp { + --control-slider-thickness: 100px; + --control-slider-border-radius: 24px; + --control-slider-background: -webkit-linear-gradient( + top, + rgb(166, 209, 255) 0%, + white 50%, + rgb(255, 160, 0) 100% + ); + --control-slider-background-opacity: 1; + } + + hr { + border-color: var(--divider-color); + border-bottom: none; + margin: 16px 0; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-more-info-view-light-color-picker": MoreInfoViewLightColorPicker; + } +} diff --git a/src/dialogs/more-info/components/lights/show-view-light-color-picker.ts b/src/dialogs/more-info/components/lights/show-view-light-color-picker.ts new file mode 100644 index 0000000000..c59cc031e9 --- /dev/null +++ b/src/dialogs/more-info/components/lights/show-view-light-color-picker.ts @@ -0,0 +1,21 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; + +export interface LightColorPickerViewParams { + entityId: string; +} + +export const loadLightColorPickerView = () => + import("./ha-more-info-view-light-color-picker"); + +export const showLightColorPickerView = ( + element: HTMLElement, + title: string, + params: LightColorPickerViewParams +): void => { + fireEvent(element, "show-child-view", { + viewTag: "ha-more-info-view-light-color-picker", + viewImport: loadLightColorPickerView, + viewTitle: title, + viewParams: params, + }); +}; diff --git a/src/dialogs/more-info/const.ts b/src/dialogs/more-info/const.ts index 30ac679e33..fde9c34c5f 100644 --- a/src/dialogs/more-info/const.ts +++ b/src/dialogs/more-info/const.ts @@ -13,7 +13,8 @@ export const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"]; * Entity Domains that should always be editable; {@see shouldShowEditIcon}. * */ export const EDITABLE_DOMAINS_WITH_UNIQUE_ID = ["script"]; - +/** Domains with with new more info design. */ +export const DOMAINS_WITH_NEW_MORE_INFO = ["light"]; /** Domains with separate more info dialog. */ export const DOMAINS_WITH_MORE_INFO = [ "alarm_control_panel", diff --git a/src/dialogs/more-info/controls/more-info-light.ts b/src/dialogs/more-info/controls/more-info-light.ts index 7ce94dabbf..90befc078e 100644 --- a/src/dialogs/more-info/controls/more-info-light.ts +++ b/src/dialogs/more-info/controls/more-info-light.ts @@ -1,5 +1,12 @@ import "@material/mwc-list/mwc-list-item"; -import { mdiPalette } from "@mdi/js"; +import "@material/web/iconbutton/outlined-icon-button"; +import { + mdiCreation, + mdiLightbulb, + mdiLightbulbOff, + mdiPalette, + mdiPower, +} from "@mdi/js"; import { css, CSSResultGroup, @@ -9,26 +16,26 @@ import { TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; import { stopPropagation } from "../../../common/dom/stop_propagation"; import { supportsFeature } from "../../../common/entity/supports-feature"; +import { blankBeforePercent } from "../../../common/translations/blank_before_percent"; import "../../../components/ha-attributes"; -import "../../../components/ha-button-toggle-group"; -import "../../../components/ha-color-picker"; -import "../../../components/ha-icon-button"; -import "../../../components/ha-labeled-slider"; +import "../../../components/ha-button-menu"; import "../../../components/ha-select"; +import { UNAVAILABLE } from "../../../data/entity"; import { - getLightCurrentModeRgbColor, LightColorMode, LightEntity, LightEntityFeature, - lightIsInColorMode, + lightSupportsBrightness, lightSupportsColor, lightSupportsColorMode, - lightSupportsBrightness, } from "../../../data/light"; import type { HomeAssistant } from "../../../types"; +import "../components/ha-more-info-state-header"; +import "../components/ha-more-info-toggle"; +import "../components/lights/ha-more-info-light-brightness"; +import { showLightColorPickerView } from "../components/lights/show-view-light-color-picker"; @customElement("more-info-light") class MoreInfoLight extends LitElement { @@ -36,198 +43,152 @@ class MoreInfoLight extends LitElement { @property({ attribute: false }) public stateObj?: LightEntity; - @state() private _brightnessSliderValue = 0; + @state() private _selectedBrightness?: number; - @state() private _ctSliderValue?: number; + private _brightnessChanged(ev) { + const value = (ev.detail as any).value; + if (isNaN(value)) return; + this._selectedBrightness = value; + } - @state() private _cwSliderValue?: number; + protected updated(changedProps: PropertyValues): void { + if (changedProps.has("stateObj")) { + this._selectedBrightness = this.stateObj?.attributes.brightness + ? Math.round((this.stateObj.attributes.brightness * 100) / 255) + : undefined; + } + } - @state() private _wwSliderValue?: number; - - @state() private _wvSliderValue?: number; - - @state() private _colorBrightnessSliderValue?: number; - - @state() private _brightnessAdjusted?: number; - - @state() private _hueSegments = 24; - - @state() private _saturationSegments = 8; - - @state() private _colorPickerColor?: [number, number, number]; - - @state() private _mode?: "color" | LightColorMode; - - protected render(): TemplateResult { + protected render(): TemplateResult | null { if (!this.hass || !this.stateObj) { - return html``; + return null; } - const supportsTemp = lightSupportsColorMode( + const supportsColorTemp = lightSupportsColorMode( this.stateObj, LightColorMode.COLOR_TEMP ); - const supportsWhite = lightSupportsColorMode( + const supportsColor = lightSupportsColor(this.stateObj); + + const supportsBrightness = lightSupportsBrightness(this.stateObj); + + const supportsEffects = supportsFeature( this.stateObj, - LightColorMode.WHITE + LightEntityFeature.EFFECT ); - const supportsRgbww = lightSupportsColorMode( - this.stateObj, - LightColorMode.RGBWW - ); - - const supportsRgbw = - !supportsRgbww && - lightSupportsColorMode(this.stateObj, LightColorMode.RGBW); - - const supportsColor = - supportsRgbww || supportsRgbw || lightSupportsColor(this.stateObj); + const stateOverride = this._selectedBrightness + ? `${Math.round(this._selectedBrightness)}${blankBeforePercent( + this.hass!.locale + )}%` + : undefined; return html`
- ${lightSupportsBrightness(this.stateObj) - ? html` - - ` - : ""} - ${this.stateObj.state === "on" - ? html` - ${supportsTemp || supportsColor ? html`
` : ""} - ${supportsColor && (supportsTemp || supportsWhite) - ? html`` - : ""} - ${supportsTemp && - ((!supportsColor && !supportsWhite) || - this._mode === LightColorMode.COLOR_TEMP) - ? html` - - ` - : ""} - ${supportsColor && - ((!supportsTemp && !supportsWhite) || this._mode === "color") - ? html` -
- - - -
- - ${supportsRgbw || supportsRgbww - ? html` + + ${ + supportsBrightness + ? html` + + + ` + : html` + + ` + } + ${ + supportsColorTemp || + supportsColor || + supportsEffects || + supportsBrightness + ? html` +
+ ${supportsBrightness + ? html` + ` - : ""} - ${supportsRgbw - ? html` - + + + ` + : null} + ${supportsColorTemp || supportsColor + ? html` + + + + ` + : null} + ${supportsEffects + ? html` + + - ` - : ""} - ${supportsRgbww - ? html` - - - ` - : ""} - ` - : ""} - ${supportsFeature(this.stateObj, LightEntityFeature.EFFECT) && - this.stateObj!.attributes.effect_list?.length - ? html` -
- - ${this.stateObj.attributes.effect_list.map( - (effect: string) => html` - - ${effect} - - ` - )} - - ` - : ""} - ` - : ""} + > + +
+ ${this.stateObj.attributes.effect_list!.map( + (effect: string) => html` + + ${effect} + + ` + )} +
+ ` + : null} +
+ ` + : null + } ) { - super.willUpdate(changedProps); - - if (!changedProps.has("stateObj")) { - return; - } - const stateObj = this.stateObj! as LightEntity; - const oldStateObj = changedProps.get("stateObj") as LightEntity | undefined; - - if (stateObj.state === "on") { - // Don't change tab when the color mode changes - if ( - oldStateObj?.entity_id !== stateObj.entity_id || - oldStateObj?.state !== stateObj.state - ) { - this._mode = lightIsInColorMode(this.stateObj!) - ? "color" - : this.stateObj!.attributes.color_mode; - } - - let brightnessAdjust = 100; - this._brightnessAdjusted = undefined; - if ( - stateObj.attributes.color_mode === LightColorMode.RGB && - !lightSupportsColorMode(stateObj, LightColorMode.RGBWW) && - !lightSupportsColorMode(stateObj, LightColorMode.RGBW) - ) { - const maxVal = Math.max(...stateObj.attributes.rgb_color!); - if (maxVal < 255) { - this._brightnessAdjusted = maxVal; - brightnessAdjust = (this._brightnessAdjusted / 255) * 100; - } - } - this._brightnessSliderValue = Math.round( - ((stateObj.attributes.brightness || 0) * brightnessAdjust) / 255 - ); - this._ctSliderValue = stateObj.attributes.color_temp_kelvin; - this._wvSliderValue = - stateObj.attributes.color_mode === LightColorMode.RGBW - ? Math.round((stateObj.attributes.rgbw_color![3] * 100) / 255) - : undefined; - this._cwSliderValue = - stateObj.attributes.color_mode === LightColorMode.RGBWW - ? Math.round((stateObj.attributes.rgbww_color![3] * 100) / 255) - : undefined; - this._wwSliderValue = - stateObj.attributes.color_mode === LightColorMode.RGBWW - ? Math.round((stateObj.attributes.rgbww_color![4] * 100) / 255) - : undefined; - - const currentRgbColor = getLightCurrentModeRgbColor(stateObj); - - this._colorBrightnessSliderValue = currentRgbColor - ? Math.round((Math.max(...currentRgbColor.slice(0, 3)) * 100) / 255) - : undefined; - - this._colorPickerColor = currentRgbColor?.slice(0, 3) as [ - number, - number, - number - ]; - } else { - this._brightnessSliderValue = 0; - } - } - - private _toggleButtons = memoizeOne( - (supportsTemp: boolean, supportsWhite: boolean) => { - const modes = [{ label: "Color", value: "color" }]; - if (supportsTemp) { - modes.push({ label: "Temperature", value: LightColorMode.COLOR_TEMP }); - } - if (supportsWhite) { - modes.push({ label: "White", value: LightColorMode.WHITE }); - } - return modes; - } - ); - - private _modeChanged(ev: CustomEvent) { - this._mode = ev.detail.value; - } - - private _effectChanged(ev) { - const newVal = ev.target.value; - - if (!newVal || this.stateObj!.attributes.effect === newVal) { - return; - } - - this.hass.callService("light", "turn_on", { + private _toggle = () => { + this.hass.callService("light", "toggle", { entity_id: this.stateObj!.entity_id, - effect: newVal, }); - } + }; - private _brightnessSliderChanged(ev: CustomEvent) { - const bri = Number((ev.target as any).value); - - if (isNaN(bri)) { - return; - } - - this._brightnessSliderValue = bri; - - if (this._mode === LightColorMode.WHITE) { - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - white: Math.min(255, Math.round((bri * 255) / 100)), - }); - return; - } - - if (this._brightnessAdjusted) { - const rgb = - this.stateObj!.attributes.rgb_color || - ([0, 0, 0] as [number, number, number]); - - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - brightness_pct: bri, - rgb_color: this._adjustColorBrightness( - rgb, - this._brightnessAdjusted, - true - ), - }); - return; - } - - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - brightness_pct: bri, - }); - } - - private _ctSliderChanged(ev: CustomEvent) { - const ct = Number((ev.target as any).value); - - if (isNaN(ct)) { - return; - } - - this._ctSliderValue = ct; - - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - color_temp_kelvin: ct, - }); - } - - private _wvSliderChanged(ev: CustomEvent) { - const target = ev.target as any; - let wv = Number(target.value); - const name = target.name; - - if (isNaN(wv)) { - return; - } - - if (name === "wv") { - this._wvSliderValue = wv; - } else if (name === "cw") { - this._cwSliderValue = wv; - } else if (name === "ww") { - this._wwSliderValue = wv; - } - - wv = Math.min(255, Math.round((wv * 255) / 100)); - - const rgb = getLightCurrentModeRgbColor(this.stateObj!); - - if (name === "wv") { - const rgbw_color = rgb || [0, 0, 0, 0]; - rgbw_color[3] = wv; - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - rgbw_color, - }); - return; - } - - const rgbww_color = rgb || [0, 0, 0, 0, 0]; - while (rgbww_color.length < 5) { - rgbww_color.push(0); - } - rgbww_color[name === "cw" ? 3 : 4] = wv; - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - rgbww_color, - }); - } - - private _colorBrightnessSliderChanged(ev: CustomEvent) { - const target = ev.target as any; - let value = Number(target.value); - - if (isNaN(value)) { - return; - } - - const oldValue = this._colorBrightnessSliderValue; - this._colorBrightnessSliderValue = value; - - value = (value * 255) / 100; - - const rgb = (getLightCurrentModeRgbColor(this.stateObj!)?.slice(0, 3) || [ - 255, 255, 255, - ]) as [number, number, number]; - - this._setRgbWColor( - this._adjustColorBrightness( - // first normalize the value - oldValue - ? this._adjustColorBrightness(rgb, (oldValue * 255) / 100, true) - : rgb, - value - ) + private _showLightColorPickerView = () => { + showLightColorPickerView( + this, + this.hass.localize( + "ui.dialogs.more_info_control.light.color_picker.title" + ), + { + entityId: this.stateObj!.entity_id, + } ); - } + }; - private _segmentClick() { - if (this._hueSegments === 24 && this._saturationSegments === 8) { - this._hueSegments = 0; - this._saturationSegments = 0; - } else { - this._hueSegments = 24; - this._saturationSegments = 8; + private _handleEffectButton(ev) { + ev.stopPropagation(); + ev.preventDefault(); + + const index = ev.detail.index; + const effect = this.stateObj!.attributes.effect_list![index]; + + if (!effect || this.stateObj!.attributes.effect === effect) { + return; } - } - private _adjustColorBrightness( - rgbColor: [number, number, number], - value?: number, - invert = false - ) { - if (value !== undefined && value !== 255) { - let ratio = value / 255; - if (invert) { - ratio = 1 / ratio; - } - rgbColor[0] = Math.min(255, Math.round(rgbColor[0] * ratio)); - rgbColor[1] = Math.min(255, Math.round(rgbColor[1] * ratio)); - rgbColor[2] = Math.min(255, Math.round(rgbColor[2] * ratio)); - } - return rgbColor; - } - - private _setRgbWColor(rgbColor: [number, number, number]) { - if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW)) { - const rgbww_color: [number, number, number, number, number] = this - .stateObj!.attributes.rgbww_color - ? [...this.stateObj!.attributes.rgbww_color] - : [0, 0, 0, 0, 0]; - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - rgbww_color: rgbColor.concat(rgbww_color.slice(3)), - }); - } else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW)) { - const rgbw_color: [number, number, number, number] = this.stateObj! - .attributes.rgbw_color - ? [...this.stateObj!.attributes.rgbw_color] - : [0, 0, 0, 0]; - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - rgbw_color: rgbColor.concat(rgbw_color.slice(3)), - }); - } - } - - /** - * Called when a new color has been picked. - * should be throttled with the 'throttle=' attribute of the color picker - */ - private _colorPicked( - ev: CustomEvent<{ - hs: { h: number; s: number }; - rgb: { r: number; g: number; b: number }; - }> - ) { - this._colorPickerColor = [ - ev.detail.rgb.r, - ev.detail.rgb.g, - ev.detail.rgb.b, - ]; - - if ( - lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW) || - lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW) - ) { - this._setRgbWColor( - this._colorBrightnessSliderValue - ? this._adjustColorBrightness( - [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b], - (this._colorBrightnessSliderValue * 255) / 100 - ) - : [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b] - ); - } else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGB)) { - const rgb_color: [number, number, number] = [ - ev.detail.rgb.r, - ev.detail.rgb.g, - ev.detail.rgb.b, - ]; - if (this._brightnessAdjusted) { - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - brightness_pct: this._brightnessSliderValue, - rgb_color: this._adjustColorBrightness( - rgb_color, - this._brightnessAdjusted, - true - ), - }); - } else { - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - rgb_color, - }); - } - } else { - this.hass.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, - hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100], - }); - } + this.hass.callService("light", "turn_on", { + entity_id: this.stateObj!.entity_id, + effect, + }); } static get styles(): CSSResultGroup { @@ -573,52 +241,31 @@ class MoreInfoLight extends LitElement { align-items: center; } - .content > * { + .buttons { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 12px; + } + .buttons > * { + margin: 4px; + } + + ha-more-info-light-brightness, + ha-more-info-light-toggle { + margin-bottom: 24px; + } + + ha-attributes { width: 100%; } - .color_temp { - --ha-slider-background: -webkit-linear-gradient( - var(--float-end), - rgb(166, 209, 255) 0%, - white 50%, - rgb(255, 160, 0) 100% - ); - /* The color temp minimum value shouldn't be rendered differently. It's not "off". */ - --paper-slider-knob-start-border-color: var(--primary-color); - margin-bottom: 4px; - } - - .segmentationContainer { - position: relative; - max-height: 500px; - display: flex; - justify-content: center; - } - - ha-button-toggle-group { - margin-bottom: 8px; - } - - ha-color-picker { - --ha-color-picker-wheel-borderwidth: 5; - --ha-color-picker-wheel-bordercolor: white; - --ha-color-picker-wheel-shadow: none; - --ha-color-picker-marker-borderwidth: 2; - --ha-color-picker-marker-bordercolor: white; - } - - .segmentationButton { - position: absolute; - top: 5%; - left: 0; - color: var(--secondary-text-color); - } - - hr { - border-color: var(--divider-color); - border-bottom: none; - margin: 16px 0; + md-outlined-icon-button-toggle, + md-outlined-icon-button { + --md-sys-color-on-surface: var(--secondary-text-color); + --md-sys-color-on-surface-variant: var(--secondary-text-color); + --md-sys-color-on-surface-rgb: var(--rgb-secondary-text-color); + --md-sys-color-outline: var(--secondary-text-color); } `; } diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index 83cb151925..94a3942a2c 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -11,6 +11,7 @@ import type { HassEntity } from "home-assistant-js-websocket"; import { css, html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; import { cache } from "lit/directives/cache"; +import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../common/dom/fire_event"; import { stopPropagation } from "../../common/dom/stop_propagation"; import { computeDomain } from "../../common/entity/compute_domain"; @@ -31,6 +32,7 @@ import { HomeAssistant } from "../../types"; import { computeShowHistoryComponent, computeShowLogBookComponent, + DOMAINS_WITH_NEW_MORE_INFO, DOMAINS_WITH_MORE_INFO, EDITABLE_DOMAINS_WITH_ID, EDITABLE_DOMAINS_WITH_UNIQUE_ID, @@ -50,6 +52,19 @@ export interface MoreInfoDialogParams { type View = "info" | "history" | "settings" | "related"; +type ChildView = { + viewTag: string; + viewTitle?: string; + viewImport?: () => Promise; + viewParams?: any; +}; + +declare global { + interface HASSDomEvents { + "show-child-view": ChildView; + } +} + @customElement("ha-more-info-dialog") export class MoreInfoDialog extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -60,6 +75,8 @@ export class MoreInfoDialog extends LitElement { @state() private _currView: View = "info"; + @state() private _childView?: ChildView; + public showDialog(params: MoreInfoDialogParams) { this._entityId = params.entityId; if (!this._entityId) { @@ -67,11 +84,13 @@ export class MoreInfoDialog extends LitElement { return; } this._currView = params.view || "info"; + this._childView = undefined; this.large = false; } public closeDialog() { this._entityId = undefined; + this._childView = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -110,8 +129,12 @@ export class MoreInfoDialog extends LitElement { return entity?.device_id ?? null; } - private back() { - this._currView = "info"; + private _goBack() { + if (this._childView) { + this._childView = undefined; + } else { + this._currView = "info"; + } } private _goToHistory() { @@ -122,6 +145,14 @@ export class MoreInfoDialog extends LitElement { this._currView = "settings"; } + private async _showChildView(ev: CustomEvent): Promise { + const view = ev.detail as ChildView; + if (view.viewImport) { + await view.viewImport(); + } + this._childView = view; + } + private _goToDevice(ev): void { if (!shouldHandleRequestSelectedEvent(ev)) return; const deviceId = this._getDeviceId(); @@ -167,17 +198,21 @@ export class MoreInfoDialog extends LitElement { const deviceId = this._getDeviceId(); + const title = this._childView?.viewTitle ?? name; + + const isInfoView = this._currView === "info" && !this._childView; + return html`
- ${this._currView === "info" + ${isInfoView ? html` `} -
- ${name} -
- ${this._currView === "info" + ${!isInfoView || !DOMAINS_WITH_NEW_MORE_INFO.includes(domain) + ? html`
+ ${title} +
` + : null} + ${isInfoView ? html` ${this.shouldShowHistory(domain) ? html` @@ -291,39 +328,50 @@ export class MoreInfoDialog extends LitElement { : null}
- -
- ${cache( - this._currView === "info" - ? html` - - ` - : this._currView === "history" - ? html` - - ` - : this._currView === "settings" - ? html` - - ` - : html` - - ` - )} +
+ ${this._childView + ? dynamicElement(this._childView.viewTag, { + hass: this.hass, + params: this._childView.viewParams, + }) + : cache( + this._currView === "info" + ? html` + + ` + : this._currView === "history" + ? html` + + ` + : this._currView === "settings" + ? html` + + ` + : this._currView === "related" + ? html` + + ` + : null + )}
`; @@ -338,6 +386,10 @@ export class MoreInfoDialog extends LitElement { super.updated(changedProps); if (changedProps.has("_currView")) { this.setAttribute("view", this._currView); + this._childView = undefined; + } + if (changedProps.has("_childView")) { + this.toggleAttribute("has-child-view", !!this._childView); } } @@ -374,10 +426,6 @@ export class MoreInfoDialog extends LitElement { var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); } - ha-dialog .content { - padding: var(--content-padding); - } - :host([view="settings"]) ha-dialog { --content-padding: 0; } @@ -388,6 +436,14 @@ export class MoreInfoDialog extends LitElement { --video-max-height: calc(100vh - 65px - 72px); } + :host([has-child-view]) ha-dialog { + --content-padding: 0; + } + + .content { + padding: var(--content-padding); + } + .main-title { overflow: hidden; text-overflow: ellipsis; diff --git a/src/dialogs/more-info/ha-more-info-info.ts b/src/dialogs/more-info/ha-more-info-info.ts index 85cb076a4e..3461c7e314 100644 --- a/src/dialogs/more-info/ha-more-info-info.ts +++ b/src/dialogs/more-info/ha-more-info-info.ts @@ -12,6 +12,7 @@ import { computeShowLogBookComponent, DOMAINS_NO_INFO, DOMAINS_WITH_MORE_INFO, + DOMAINS_WITH_NEW_MORE_INFO, } from "./const"; import "./ha-more-info-history"; import "./ha-more-info-logbook"; @@ -47,7 +48,8 @@ export class MoreInfoInfo extends LitElement { )} ` : ""} - ${DOMAINS_NO_INFO.includes(domain) + ${DOMAINS_NO_INFO.includes(domain) || + DOMAINS_WITH_NEW_MORE_INFO.includes(domain) ? "" : html` import("./controls/more-info-alarm_control_panel"), @@ -33,7 +33,6 @@ const LAZY_LOADED_MORE_INFO_CONTROL = { export const stateMoreInfoType = (stateObj: HassEntity): string => { const domain = computeStateDomain(stateObj); - return domainMoreInfoType(domain); }; diff --git a/src/translations/en.json b/src/translations/en.json index deb63a5d80..86166dd30e 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -841,6 +841,9 @@ "last_changed": "Last changed", "last_updated": "Last updated", "show_more": "Show more", + "turn_on": "Turn on", + "turn_off": "Turn off", + "toggle": "Toggle", "script": { "last_action": "Last action", "last_triggered": "Last triggered" @@ -892,6 +895,19 @@ }, "zone": { "graph_unit": "People home" + }, + "light": { + "toggle": "Toggle", + "change_color": "Change color", + "select_effect": "Select effect", + "brightness": "Brightness", + "color_picker": { + "title": "Change color", + "mode": { + "color": "Color", + "color_temp": "Temperature" + } + } } }, "entity_registry": { diff --git a/yarn.lock b/yarn.lock index 834e424c65..a29047c3cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3095,6 +3095,16 @@ __metadata: languageName: node linkType: hard +"@material/web@npm:=1.0.0-pre.2": + version: 1.0.0-pre.2 + resolution: "@material/web@npm:1.0.0-pre.2" + dependencies: + lit: ^2.3.0 + tslib: ^2.4.0 + checksum: 7c6733fae5fb67c43d7c49fab70f7893defd95e4fcbe996d06057882e47c0121760546cc5d1c407a9dbd11c5f02f3f278016c52922e6a9e97db0c0b52d7133f2 + languageName: node + linkType: hard + "@mdi/js@npm:7.1.96": version: 7.1.96 resolution: "@mdi/js@npm:7.1.96" @@ -9540,6 +9550,7 @@ fsevents@^1.2.7: "@material/mwc-textfield": ^0.27.0 "@material/mwc-top-app-bar-fixed": ^0.27.0 "@material/top-app-bar": =14.0.0-canary.53b3cad2f.0 + "@material/web": =1.0.0-pre.2 "@mdi/js": 7.1.96 "@mdi/svg": 7.1.96 "@octokit/auth-oauth-device": ^4.0.4 @@ -11336,7 +11347,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"lit@npm:^2.0.0, lit@npm:^2.0.0-rc.2, lit@npm:^2.2.1, lit@npm:^2.5.0, lit@npm:^2.6.1": +"lit@npm:^2.0.0, lit@npm:^2.0.0-rc.2, lit@npm:^2.2.1, lit@npm:^2.3.0, lit@npm:^2.5.0, lit@npm:^2.6.1": version: 2.6.1 resolution: "lit@npm:2.6.1" dependencies: