diff --git a/.prettierrc.js b/.prettierrc.js index 5e6050e41..31e54114a 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,12 +1,3 @@ module.exports = { - arrowParens: "always", - bracketSpacing: true, - bracketSameLine: false, - endOfLine: "lf", - printWidth: 100, - semi: true, - singleQuote: false, - tabWidth: 4, - trailingComma: "es5", - useTabs: false, + trailingComma: "es5", }; diff --git a/src/cards/alarm-control-panel-card/alarm-control-panel-card-config.ts b/src/cards/alarm-control-panel-card/alarm-control-panel-card-config.ts index c0e3936dd..a3e9fc3c1 100644 --- a/src/cards/alarm-control-panel-card/alarm-control-panel-card-config.ts +++ b/src/cards/alarm-control-panel-card/alarm-control-panel-card-config.ts @@ -1,30 +1,47 @@ -import { array, assign, boolean, deprecated, object, optional } from "superstruct"; +import { + array, + assign, + boolean, + deprecated, + object, + optional, +} from "superstruct"; import { LovelaceCardConfig } from "../../ha"; -import { ActionsSharedConfig, actionsSharedConfigStruct } from "../../shared/config/actions-config"; import { - AppearanceSharedConfig, - appearanceSharedConfigStruct, + ActionsSharedConfig, + actionsSharedConfigStruct, +} from "../../shared/config/actions-config"; +import { + AppearanceSharedConfig, + appearanceSharedConfigStruct, } from "../../shared/config/appearance-config"; -import { EntitySharedConfig, entitySharedConfigStruct } from "../../shared/config/entity-config"; +import { + EntitySharedConfig, + entitySharedConfigStruct, +} from "../../shared/config/entity-config"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; import { AlarmMode } from "../../ha/data/alarm_control_panel"; export type AlarmControlPanelCardConfig = LovelaceCardConfig & - EntitySharedConfig & - AppearanceSharedConfig & - ActionsSharedConfig & { - states?: AlarmMode[]; - }; + EntitySharedConfig & + AppearanceSharedConfig & + ActionsSharedConfig & { + states?: AlarmMode[]; + }; export const alarmControlPanelCardCardConfigStruct = assign( - lovelaceCardConfigStruct, - assign(entitySharedConfigStruct, appearanceSharedConfigStruct, actionsSharedConfigStruct), - object({ - states: optional(array()), - show_keypad: deprecated(optional(boolean()), (_value, ctx) => { - console.warn( - `🍄 "${ctx.path}" option is deprecated and no longer available. Remove it from your YAML configuration or use the built-in Home Assistant alarm panel card if you want keypad.` - ); - }), - }) + lovelaceCardConfigStruct, + assign( + entitySharedConfigStruct, + appearanceSharedConfigStruct, + actionsSharedConfigStruct + ), + object({ + states: optional(array()), + show_keypad: deprecated(optional(boolean()), (_value, ctx) => { + console.warn( + `🍄 "${ctx.path}" option is deprecated and no longer available. Remove it from your YAML configuration or use the built-in Home Assistant alarm panel card if you want keypad.` + ); + }), + }) ); diff --git a/src/cards/alarm-control-panel-card/alarm-control-panel-card-editor.ts b/src/cards/alarm-control-panel-card/alarm-control-panel-card-editor.ts index 30597036f..0ef5f0881 100644 --- a/src/cards/alarm-control-panel-card/alarm-control-panel-card-editor.ts +++ b/src/cards/alarm-control-panel-card/alarm-control-panel-card-editor.ts @@ -12,79 +12,103 @@ import { HaFormSchema } from "../../utils/form/ha-form"; import { UiAction } from "../../utils/form/ha-selector"; import { loadHaComponents } from "../../utils/loader"; import { - AlarmControlPanelCardConfig, - alarmControlPanelCardCardConfigStruct, + AlarmControlPanelCardConfig, + alarmControlPanelCardCardConfigStruct, } from "./alarm-control-panel-card-config"; -import { ALARM_CONTROl_PANEL_CARD_EDITOR_NAME, ALARM_CONTROl_PANEL_ENTITY_DOMAINS } from "./const"; +import { + ALARM_CONTROl_PANEL_CARD_EDITOR_NAME, + ALARM_CONTROl_PANEL_ENTITY_DOMAINS, +} from "./const"; -const actions: UiAction[] = ["more-info", "navigate", "url", "call-service", "assist", "none"]; +const actions: UiAction[] = [ + "more-info", + "navigate", + "url", + "call-service", + "assist", + "none", +]; -const states = ["armed_home", "armed_away", "armed_night", "armed_vacation", "armed_custom_bypass"]; +const states = [ + "armed_home", + "armed_away", + "armed_night", + "armed_vacation", + "armed_custom_bypass", +]; const computeSchema = memoizeOne((localize: LocalizeFunc): HaFormSchema[] => [ - { name: "entity", selector: { entity: { domain: ALARM_CONTROl_PANEL_ENTITY_DOMAINS } } }, - { name: "name", selector: { text: {} } }, - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - ...APPEARANCE_FORM_SCHEMA, - { - type: "multi_select", - name: "states", - options: states.map((state) => [ - state, - localize(`ui.card.alarm_control_panel.${state.replace("armed", "arm")}`), - ]) as [string, string][], - }, - ...computeActionsFormSchema(actions), + { + name: "entity", + selector: { entity: { domain: ALARM_CONTROl_PANEL_ENTITY_DOMAINS } }, + }, + { name: "name", selector: { text: {} } }, + { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, + ...APPEARANCE_FORM_SCHEMA, + { + type: "multi_select", + name: "states", + options: states.map((state) => [ + state, + localize(`ui.card.alarm_control_panel.${state.replace("armed", "arm")}`), + ]) as [string, string][], + }, + ...computeActionsFormSchema(actions), ]); @customElement(ALARM_CONTROl_PANEL_CARD_EDITOR_NAME) -export class SwitchCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: AlarmControlPanelCardConfig; - - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } - - public setConfig(config: AlarmControlPanelCardConfig): void { - assert(config, alarmControlPanelCardCardConfigStruct); - this._config = config; - } +export class SwitchCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: AlarmControlPanelCardConfig; - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } - const schema = computeSchema(this.hass!.localize); + public setConfig(config: AlarmControlPanelCardConfig): void { + assert(config, alarmControlPanelCardCardConfigStruct); + this._config = config; + } - return html` - - `; + protected render() { + if (!this.hass || !this._config) { + return nothing; } - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + const schema = computeSchema(this.hass!.localize); - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - if (schema.name === "states") { - return this.hass!.localize( - "ui.panel.lovelace.editor.card.alarm-panel.available_states" - ); - } + return html` + + `; + } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); + } + if (schema.name === "states") { + return this.hass!.localize( + "ui.panel.lovelace.editor.card.alarm-panel.available_states" + ); } + + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/alarm-control-panel-card/alarm-control-panel-card.ts b/src/cards/alarm-control-panel-card/alarm-control-panel-card.ts index 682510e63..127db3d6a 100644 --- a/src/cards/alarm-control-panel-card/alarm-control-panel-card.ts +++ b/src/cards/alarm-control-panel-card/alarm-control-panel-card.ts @@ -1,19 +1,30 @@ import { HassEntity } from "home-assistant-js-websocket"; -import { css, CSSResultGroup, html, nothing, PropertyValues, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + nothing, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - computeRTL, - handleAction, - hasAction, - HomeAssistant, - LovelaceCard, - LovelaceCardEditor, + actionHandler, + ActionHandlerEvent, + computeRTL, + handleAction, + hasAction, + HomeAssistant, + LovelaceCard, + LovelaceCardEditor, } from "../../ha"; -import { ALARM_MODES, AlarmMode, setProtectedAlarmControlPanelMode } from "../../ha/data/alarm_control_panel"; +import { + ALARM_MODES, + AlarmMode, + setProtectedAlarmControlPanelMode, +} from "../../ha/data/alarm_control_panel"; import "../../shared/badge-icon"; import "../../shared/button"; import "../../shared/button-group"; @@ -28,21 +39,27 @@ import { registerCustomCard } from "../../utils/custom-cards"; import { computeEntityPicture } from "../../utils/info"; import { AlarmControlPanelCardConfig } from "./alarm-control-panel-card-config"; import { - ALARM_CONTROl_PANEL_CARD_EDITOR_NAME, - ALARM_CONTROl_PANEL_CARD_NAME, - ALARM_CONTROl_PANEL_ENTITY_DOMAINS, + ALARM_CONTROl_PANEL_CARD_EDITOR_NAME, + ALARM_CONTROl_PANEL_CARD_NAME, + ALARM_CONTROl_PANEL_ENTITY_DOMAINS, } from "./const"; -import { getStateColor, hasCode, isActionsAvailable, isDisarmed, shouldPulse } from "./utils"; +import { + getStateColor, + hasCode, + isActionsAvailable, + isDisarmed, + shouldPulse, +} from "./utils"; registerCustomCard({ - type: ALARM_CONTROl_PANEL_CARD_NAME, - name: "Mushroom Alarm Control Panel Card", - description: "Card for alarm control panel", + type: ALARM_CONTROl_PANEL_CARD_NAME, + name: "Mushroom Alarm Control Panel Card", + description: "Card for alarm control panel", }); type ActionButtonType = { - mode: AlarmMode; - disabled?: boolean; + mode: AlarmMode; + disabled?: boolean; }; /* @@ -52,141 +69,149 @@ type ActionButtonType = { @customElement(ALARM_CONTROl_PANEL_CARD_NAME) export class AlarmControlPanelCard - extends MushroomBaseCard - implements LovelaceCard + extends MushroomBaseCard + implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./alarm-control-panel-card-editor"); - return document.createElement(ALARM_CONTROl_PANEL_CARD_EDITOR_NAME) as LovelaceCardEditor; - } - - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - const panels = entities.filter((e) => - ALARM_CONTROl_PANEL_ENTITY_DOMAINS.includes(e.split(".")[0]) - ); - return { - type: `custom:${ALARM_CONTROl_PANEL_CARD_NAME}`, - entity: panels[0], - states: ["armed_home", "armed_away"], - }; - } - - protected get hasControls(): boolean { - return Boolean(this._config?.states?.length); + public static async getConfigElement(): Promise { + await import("./alarm-control-panel-card-editor"); + return document.createElement( + ALARM_CONTROl_PANEL_CARD_EDITOR_NAME + ) as LovelaceCardEditor; + } + + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + const panels = entities.filter((e) => + ALARM_CONTROl_PANEL_ENTITY_DOMAINS.includes(e.split(".")[0]) + ); + return { + type: `custom:${ALARM_CONTROl_PANEL_CARD_NAME}`, + entity: panels[0], + states: ["armed_home", "armed_away"], + }; + } + + protected get hasControls(): boolean { + return Boolean(this._config?.states?.length); + } + + private _onTap(e: MouseEvent, mode: AlarmMode): void { + e.stopPropagation(); + setProtectedAlarmControlPanelMode(this, this.hass!, this._stateObj!, mode); + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + protected render() { + if (!this.hass || !this._config || !this._config.entity) { + return nothing; } - private _onTap(e: MouseEvent, mode: AlarmMode): void { - e.stopPropagation(); - setProtectedAlarmControlPanelMode(this, this.hass!, this._stateObj!, mode); - } + const stateObj = this._stateObj; - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); + if (!stateObj) { + return this.renderNotFound(this._config); } - protected render() { - if (!this.hass || !this._config || !this._config.entity) { - return nothing; + const name = this._config.name || stateObj.attributes.friendly_name || ""; + const icon = this._config.icon; + const appearance = computeAppearance(this._config); + const picture = computeEntityPicture(stateObj, appearance.icon_type); + + const actions: ActionButtonType[] = + this._config.states && this._config.states.length > 0 + ? isDisarmed(stateObj) + ? this._config.states.map((state) => ({ mode: state })) + : [{ mode: "disarmed" }] + : []; + + const isActionEnabled = isActionsAvailable(stateObj); + + const rtl = computeRTL(this.hass); + + return html` + + + + ${picture + ? this.renderPicture(picture) + : this.renderIcon(stateObj, icon)} + ${this.renderBadge(stateObj)} + ${this.renderStateInfo(stateObj, appearance, name)}; + + ${actions.length > 0 + ? html` + + ${actions.map( + (action) => html` + this._onTap(e, action.mode)} + .disabled=${!isActionEnabled} + > + + + + ` + )} + + ` + : nothing} + + + `; + } + + protected renderIcon(stateObj: HassEntity, icon?: string): TemplateResult { + const color = getStateColor(stateObj.state); + const shapePulse = shouldPulse(stateObj.state); + const iconStyle = { + "--icon-color": `rgb(${color})`, + "--shape-color": `rgba(${color}, 0.2)`, + }; + return html` + + + + `; + } + + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + mushroom-state-item { + cursor: pointer; } - - const stateObj = this._stateObj; - - if (!stateObj) { - return this.renderNotFound(this._config); + mushroom-shape-icon.pulse { + --shape-animation: 1s ease 0s infinite normal none running pulse; } - - const name = this._config.name || stateObj.attributes.friendly_name || ""; - const icon = this._config.icon; - const appearance = computeAppearance(this._config); - const picture = computeEntityPicture(stateObj, appearance.icon_type); - - const actions: ActionButtonType[] = - this._config.states && this._config.states.length > 0 - ? isDisarmed(stateObj) - ? this._config.states.map((state) => ({ mode: state })) - : [{ mode: "disarmed" }] - : []; - - const isActionEnabled = isActionsAvailable(stateObj); - - const rtl = computeRTL(this.hass); - - return html` - - - - ${picture ? this.renderPicture(picture) : this.renderIcon(stateObj, icon)} - ${this.renderBadge(stateObj)} - ${this.renderStateInfo(stateObj, appearance, name)}; - - ${actions.length > 0 - ? html` - - ${actions.map( - (action) => html` - this._onTap(e, action.mode)} - .disabled=${!isActionEnabled} - > - - - - ` - )} - - ` - : nothing} - - - `; - } - - protected renderIcon(stateObj: HassEntity, icon?: string): TemplateResult { - const color = getStateColor(stateObj.state); - const shapePulse = shouldPulse(stateObj.state); - const iconStyle = { - "--icon-color": `rgb(${color})`, - "--shape-color": `rgba(${color}, 0.2)`, - }; - return html` - - - - `; - } - - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - mushroom-state-item { - cursor: pointer; - } - mushroom-shape-icon.pulse { - --shape-animation: 1s ease 0s infinite normal none running pulse; - } - `, - ]; - } + `, + ]; + } } diff --git a/src/cards/alarm-control-panel-card/const.ts b/src/cards/alarm-control-panel-card/const.ts index 264edf6dc..0f05fe37f 100644 --- a/src/cards/alarm-control-panel-card/const.ts +++ b/src/cards/alarm-control-panel-card/const.ts @@ -5,19 +5,19 @@ export const ALARM_CONTROl_PANEL_CARD_EDITOR_NAME = `${ALARM_CONTROl_PANEL_CARD_ export const ALARM_CONTROl_PANEL_ENTITY_DOMAINS = ["alarm_control_panel"]; export const ALARM_CONTROL_PANEL_CARD_STATE_COLOR = { - disarmed: "var(--rgb-state-alarm-disarmed)", - armed: "var(--rgb-state-alarm-armed)", - triggered: "var(--rgb-state-alarm-triggered)", - unavailable: "var(--rgb-warning)", + disarmed: "var(--rgb-state-alarm-disarmed)", + armed: "var(--rgb-state-alarm-armed)", + triggered: "var(--rgb-state-alarm-triggered)", + unavailable: "var(--rgb-warning)", }; export const ALARM_CONTROL_PANEL_CARD_DEFAULT_STATE_COLOR = "var(--rgb-grey)"; export const ALARM_CONTROL_PANEL_CARD_STATE_SERVICE = { - disarmed: "alarm_disarm", - armed_away: "alarm_arm_away", - armed_home: "alarm_arm_home", - armed_night: "alarm_arm_night", - armed_vacation: "alarm_arm_vacation", - armed_custom_bypass: "alarm_arm_custom_bypass", + disarmed: "alarm_disarm", + armed_away: "alarm_arm_away", + armed_home: "alarm_arm_home", + armed_night: "alarm_arm_night", + armed_vacation: "alarm_arm_vacation", + armed_custom_bypass: "alarm_arm_custom_bypass", }; diff --git a/src/cards/alarm-control-panel-card/utils.ts b/src/cards/alarm-control-panel-card/utils.ts index 2e84786e8..f6723d541 100644 --- a/src/cards/alarm-control-panel-card/utils.ts +++ b/src/cards/alarm-control-panel-card/utils.ts @@ -1,34 +1,37 @@ import { HassEntity } from "home-assistant-js-websocket"; import { UNAVAILABLE } from "../../ha"; import { - ALARM_CONTROL_PANEL_CARD_DEFAULT_STATE_COLOR, - ALARM_CONTROL_PANEL_CARD_STATE_COLOR, - ALARM_CONTROL_PANEL_CARD_STATE_SERVICE, + ALARM_CONTROL_PANEL_CARD_DEFAULT_STATE_COLOR, + ALARM_CONTROL_PANEL_CARD_STATE_COLOR, + ALARM_CONTROL_PANEL_CARD_STATE_SERVICE, } from "./const"; export function getStateColor(state: string): string { - return ( - ALARM_CONTROL_PANEL_CARD_STATE_COLOR[state.split("_")[0]] ?? - ALARM_CONTROL_PANEL_CARD_DEFAULT_STATE_COLOR - ); + return ( + ALARM_CONTROL_PANEL_CARD_STATE_COLOR[state.split("_")[0]] ?? + ALARM_CONTROL_PANEL_CARD_DEFAULT_STATE_COLOR + ); } export function getStateService(state: string): string | undefined { - return ALARM_CONTROL_PANEL_CARD_STATE_SERVICE[state]; + return ALARM_CONTROL_PANEL_CARD_STATE_SERVICE[state]; } export function shouldPulse(state: string): boolean { - return ["arming", "triggered", "pending", UNAVAILABLE].indexOf(state) >= 0; + return ["arming", "triggered", "pending", UNAVAILABLE].indexOf(state) >= 0; } export function isActionsAvailable(stateObj: HassEntity) { - return UNAVAILABLE !== stateObj.state; + return UNAVAILABLE !== stateObj.state; } export function isDisarmed(stateObj: HassEntity) { - return stateObj.state === "disarmed"; + return stateObj.state === "disarmed"; } export function hasCode(stateObj: HassEntity): boolean { - return stateObj.attributes.code_format && stateObj.attributes.code_format !== "no_code"; + return ( + stateObj.attributes.code_format && + stateObj.attributes.code_format !== "no_code" + ); } diff --git a/src/cards/chips-card/chips-card-chips-editor.ts b/src/cards/chips-card/chips-card-chips-editor.ts index 8e290fddf..1e6430112 100644 --- a/src/cards/chips-card/chips-card-chips-editor.ts +++ b/src/cards/chips-card/chips-card-chips-editor.ts @@ -15,314 +15,320 @@ import { setupConditionChipComponent } from "./chips/conditional-chip"; let Sortable; declare global { - interface HASSDomEvents { - "chips-changed": { - chips: LovelaceChipConfig[]; - }; - } + interface HASSDomEvents { + "chips-changed": { + chips: LovelaceChipConfig[]; + }; + } } const NON_EDITABLE_CHIPS = new Set(["spacer"]); @customElement("mushroom-chips-card-chips-editor") export class ChipsCardEditorChips extends MushroomBaseElement { - @property({ attribute: false }) protected chips?: LovelaceChipConfig[]; + @property({ attribute: false }) protected chips?: LovelaceChipConfig[]; + + @property() protected label?: string; + + @state() private _attached = false; - @property() protected label?: string; + @state() private _renderEmptySortable = false; - @state() private _attached = false; + private _sortable?; - @state() private _renderEmptySortable = false; + public connectedCallback() { + super.connectedCallback(); + this._attached = true; + } - private _sortable?; + public disconnectedCallback() { + super.disconnectedCallback(); + this._attached = false; + } - public connectedCallback() { - super.connectedCallback(); - this._attached = true; + protected render() { + if (!this.chips || !this.hass) { + return nothing; } - public disconnectedCallback() { - super.disconnectedCallback(); - this._attached = false; + const customLocalize = setupCustomlocalize(this.hass); + + return html` +

+ ${this.label || + `${customLocalize("editor.chip.chip-picker.chips")} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.required" + )})`} +

+
+ ${guard([this.chips, this._renderEmptySortable], () => + this._renderEmptySortable + ? "" + : this.chips!.map( + (chipConf, index) => html` +
+
+ +
+ ${html` +
+
+ ${this._renderChipLabel(chipConf)} + + ${this._renderChipSecondary(chipConf)} + +
+
+ `} + ${NON_EDITABLE_CHIPS.has(chipConf.type) + ? nothing + : html` + + + + `} + + + +
+ ` + ) + )} +
+ e.stopPropagation()} + fixedMenuPosition + naturalMenuWidth + > + ${CHIP_LIST.map( + (chip) => html` + + ${customLocalize(`editor.chip.chip-picker.types.${chip}`)} + + ` + )} + + `; + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + + const attachedChanged = changedProps.has("_attached"); + const chipsChanged = changedProps.has("chips"); + + if (!chipsChanged && !attachedChanged) { + return; } - protected render() { - if (!this.chips || !this.hass) { - return nothing; - } + if (attachedChanged && !this._attached) { + // Tear down sortable, if available + this._sortable?.destroy(); + this._sortable = undefined; + return; + } - const customLocalize = setupCustomlocalize(this.hass); - - return html` -

- ${this.label || - `${customLocalize("editor.chip.chip-picker.chips")} (${this.hass!.localize( - "ui.panel.lovelace.editor.card.config.required" - )})`} -

-
- ${guard([this.chips, this._renderEmptySortable], () => - this._renderEmptySortable - ? "" - : this.chips!.map( - (chipConf, index) => html` -
-
- -
- ${html` -
-
- ${this._renderChipLabel(chipConf)} - - ${this._renderChipSecondary(chipConf)} - -
-
- `} - ${NON_EDITABLE_CHIPS.has(chipConf.type) - ? nothing - : html` - - - - `} - - - -
- ` - ) - )} -
- e.stopPropagation()} - fixedMenuPosition - naturalMenuWidth - > - ${CHIP_LIST.map( - (chip) => html` - - ${customLocalize(`editor.chip.chip-picker.types.${chip}`)} - - ` - )} - - `; + if (!this._sortable && this.chips) { + this._createSortable(); + return; } - protected updated(changedProps: PropertyValues): void { - super.updated(changedProps); + if (chipsChanged) { + this._handleChipsChanged(); + } + } + + private async _handleChipsChanged() { + this._renderEmptySortable = true; + await this.updateComplete; + const container = this.shadowRoot!.querySelector(".chips")!; + while (container.lastElementChild) { + container.removeChild(container.lastElementChild); + } + this._renderEmptySortable = false; + } + + private async _createSortable() { + if (!Sortable) { + const sortableImport = await import( + "sortablejs/modular/sortable.core.esm" + ); + + Sortable = sortableImport.Sortable; + Sortable.mount(sortableImport.OnSpill); + Sortable.mount(sortableImport.AutoScroll()); + } - const attachedChanged = changedProps.has("_attached"); - const chipsChanged = changedProps.has("chips"); + this._sortable = new Sortable(this.shadowRoot!.querySelector(".chips"), { + animation: 150, + fallbackClass: "sortable-fallback", + handle: ".handle", + onEnd: async (evt: SortableEvent) => this._chipMoved(evt), + }); + } - if (!chipsChanged && !attachedChanged) { - return; - } + private async _addChips(ev: any): Promise { + const target = ev.target! as EditorTarget; + const value = target.value as string; - if (attachedChanged && !this._attached) { - // Tear down sortable, if available - this._sortable?.destroy(); - this._sortable = undefined; - return; - } + if (value === "") { + return; + } - if (!this._sortable && this.chips) { - this._createSortable(); - return; - } + let newChip: LovelaceChipConfig; - if (chipsChanged) { - this._handleChipsChanged(); - } + if (value === "conditional") { + await setupConditionChipComponent(); } - private async _handleChipsChanged() { - this._renderEmptySortable = true; - await this.updateComplete; - const container = this.shadowRoot!.querySelector(".chips")!; - while (container.lastElementChild) { - container.removeChild(container.lastElementChild); - } - this._renderEmptySortable = false; - } + // Check if a stub config exists + const elClass = getChipElementClass(value) as any; - private async _createSortable() { - if (!Sortable) { - const sortableImport = await import("sortablejs/modular/sortable.core.esm"); + if (elClass && elClass.getStubConfig) { + newChip = (await elClass.getStubConfig(this.hass)) as LovelaceChipConfig; + } else { + newChip = { type: value } as LovelaceChipConfig; + } - Sortable = sortableImport.Sortable; - Sortable.mount(sortableImport.OnSpill); - Sortable.mount(sortableImport.AutoScroll()); - } + const newConfigChips = this.chips!.concat(newChip); + target.value = ""; + fireEvent(this, "chips-changed", { + chips: newConfigChips, + }); + } - this._sortable = new Sortable(this.shadowRoot!.querySelector(".chips"), { - animation: 150, - fallbackClass: "sortable-fallback", - handle: ".handle", - onEnd: async (evt: SortableEvent) => this._chipMoved(evt), - }); + private _chipMoved(ev: SortableEvent): void { + if (ev.oldIndex === ev.newIndex) { + return; } - private async _addChips(ev: any): Promise { - const target = ev.target! as EditorTarget; - const value = target.value as string; - - if (value === "") { - return; + const newChips = this.chips!.concat(); + + newChips.splice(ev.newIndex!, 0, newChips.splice(ev.oldIndex!, 1)[0]); + + fireEvent(this, "chips-changed", { chips: newChips }); + } + + private _removeChip(ev: CustomEvent): void { + const index = (ev.currentTarget as any).index; + const newConfigChips = this.chips!.concat(); + + newConfigChips.splice(index, 1); + + fireEvent(this, "chips-changed", { + chips: newConfigChips, + }); + } + + private _editChip(ev: CustomEvent): void { + const index = (ev.currentTarget as any).index; + fireEvent(this, "edit-detail-element", { + subElementConfig: { + index, + type: "chip", + elementConfig: this.chips![index], + }, + }); + } + + private _renderChipLabel(chipConf: LovelaceChipConfig): string { + const customLocalize = setupCustomlocalize(this.hass); + return customLocalize(`editor.chip.chip-picker.types.${chipConf.type}`); + } + + private _renderChipSecondary( + chipConf: LovelaceChipConfig + ): string | undefined { + const customLocalize = setupCustomlocalize(this.hass); + if ("entity" in chipConf && chipConf.entity) { + return `${this.getEntityName(chipConf.entity) ?? chipConf.entity ?? ""}`; + } + if ("chip" in chipConf && chipConf.chip) { + const label = customLocalize( + `editor.chip.chip-picker.types.${chipConf.chip.type}` + ); + const chipSecondary = this._renderChipSecondary(chipConf.chip); + if (chipSecondary) { + return `${this._renderChipSecondary(chipConf.chip)} (via ${label})`; + } + return label; + } + return ""; + } + + private getEntityName(entity_id: string): string | undefined { + if (!this.hass) return undefined; + const stateObj = this.hass.states[entity_id] as HassEntity | undefined; + if (!stateObj) return undefined; + return stateObj.attributes.friendly_name; + } + + static get styles(): CSSResultGroup { + return [ + super.styles, + sortableStyles, + css` + .chip { + display: flex; + align-items: center; } - let newChip: LovelaceChipConfig; - - if (value === "conditional") { - await setupConditionChipComponent(); + ha-icon { + display: flex; } - // Check if a stub config exists - const elClass = getChipElementClass(value) as any; - - if (elClass && elClass.getStubConfig) { - newChip = (await elClass.getStubConfig(this.hass)) as LovelaceChipConfig; - } else { - newChip = { type: value } as LovelaceChipConfig; + mushroom-select { + width: 100%; } - const newConfigChips = this.chips!.concat(newChip); - target.value = ""; - fireEvent(this, "chips-changed", { - chips: newConfigChips, - }); - } - - private _chipMoved(ev: SortableEvent): void { - if (ev.oldIndex === ev.newIndex) { - return; + .chip .handle { + padding-right: 8px; + cursor: move; } - const newChips = this.chips!.concat(); - - newChips.splice(ev.newIndex!, 0, newChips.splice(ev.oldIndex!, 1)[0]); - - fireEvent(this, "chips-changed", { chips: newChips }); - } - - private _removeChip(ev: CustomEvent): void { - const index = (ev.currentTarget as any).index; - const newConfigChips = this.chips!.concat(); - - newConfigChips.splice(index, 1); - - fireEvent(this, "chips-changed", { - chips: newConfigChips, - }); - } - - private _editChip(ev: CustomEvent): void { - const index = (ev.currentTarget as any).index; - fireEvent(this, "edit-detail-element", { - subElementConfig: { - index, - type: "chip", - elementConfig: this.chips![index], - }, - }); - } - - private _renderChipLabel(chipConf: LovelaceChipConfig): string { - const customLocalize = setupCustomlocalize(this.hass); - return customLocalize(`editor.chip.chip-picker.types.${chipConf.type}`); - } + .chip .handle > * { + pointer-events: none; + } - private _renderChipSecondary(chipConf: LovelaceChipConfig): string | undefined { - const customLocalize = setupCustomlocalize(this.hass); - if ("entity" in chipConf && chipConf.entity) { - return `${this.getEntityName(chipConf.entity) ?? chipConf.entity ?? ""}`; + .special-row { + height: 60px; + font-size: 16px; + display: flex; + align-items: center; + justify-content: space-between; + flex-grow: 1; } - if ("chip" in chipConf && chipConf.chip) { - const label = customLocalize(`editor.chip.chip-picker.types.${chipConf.chip.type}`); - const chipSecondary = this._renderChipSecondary(chipConf.chip); - if (chipSecondary) { - return `${this._renderChipSecondary(chipConf.chip)} (via ${label})`; - } - return label; + + .special-row div { + display: flex; + flex-direction: column; } - return ""; - } - private getEntityName(entity_id: string): string | undefined { - if (!this.hass) return undefined; - const stateObj = this.hass.states[entity_id] as HassEntity | undefined; - if (!stateObj) return undefined; - return stateObj.attributes.friendly_name; - } + .remove-icon, + .edit-icon { + --mdc-icon-button-size: 36px; + color: var(--secondary-text-color); + } - static get styles(): CSSResultGroup { - return [ - super.styles, - sortableStyles, - css` - .chip { - display: flex; - align-items: center; - } - - ha-icon { - display: flex; - } - - mushroom-select { - width: 100%; - } - - .chip .handle { - padding-right: 8px; - cursor: move; - } - - .chip .handle > * { - pointer-events: none; - } - - .special-row { - height: 60px; - font-size: 16px; - display: flex; - align-items: center; - justify-content: space-between; - flex-grow: 1; - } - - .special-row div { - display: flex; - flex-direction: column; - } - - .remove-icon, - .edit-icon { - --mdc-icon-button-size: 36px; - color: var(--secondary-text-color); - } - - .secondary { - font-size: 12px; - color: var(--secondary-text-color); - } - `, - ]; - } + .secondary { + font-size: 12px; + color: var(--secondary-text-color); + } + `, + ]; + } } diff --git a/src/cards/chips-card/chips-card-editor.ts b/src/cards/chips-card/chips-card-editor.ts index 2487f32c0..6f16d3256 100644 --- a/src/cards/chips-card/chips-card-editor.ts +++ b/src/cards/chips-card/chips-card-editor.ts @@ -1,19 +1,24 @@ import { html, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; import { - any, - array, - assert, - assign, - boolean, - dynamic, - literal, - object, - optional, - string, - union, + any, + array, + assert, + assign, + boolean, + dynamic, + literal, + object, + optional, + string, + union, } from "superstruct"; -import { actionConfigStruct, fireEvent, HASSDomEvent, LovelaceCardEditor } from "../../ha"; +import { + actionConfigStruct, + fireEvent, + HASSDomEvent, + LovelaceCardEditor, +} from "../../ha"; import setupCustomlocalize from "../../localize"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; import "../../shared/editor/alignment-picker"; @@ -21,9 +26,9 @@ import { MushroomBaseElement } from "../../utils/base-element"; import { loadHaComponents } from "../../utils/loader"; import { LovelaceChipConfig } from "../../utils/lovelace/chip/types"; import { - EditorTarget, - EditSubElementEvent, - SubElementEditorConfig, + EditorTarget, + EditSubElementEvent, + SubElementEditorConfig, } from "../../utils/lovelace/editor/types"; import "../../utils/lovelace/sub-element-editor"; import { ChipsCardConfig } from "./chips-card"; @@ -31,264 +36,270 @@ import "./chips-card-chips-editor"; import { CHIPS_CARD_EDITOR_NAME } from "./const"; const actionChipConfigStruct = object({ - type: literal("action"), - icon: optional(string()), - icon_color: optional(string()), - tap_action: optional(actionConfigStruct), - hold_action: optional(actionConfigStruct), - double_tap_action: optional(actionConfigStruct), + type: literal("action"), + icon: optional(string()), + icon_color: optional(string()), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + double_tap_action: optional(actionConfigStruct), }); const backChipConfigStruct = object({ - type: literal("back"), - icon: optional(string()), - icon_color: optional(string()), + type: literal("back"), + icon: optional(string()), + icon_color: optional(string()), }); const entityChipConfigStruct = object({ - type: literal("entity"), - entity: optional(string()), - name: optional(string()), - content_info: optional(string()), - icon: optional(string()), - icon_color: optional(string()), - use_entity_picture: optional(boolean()), - tap_action: optional(actionConfigStruct), - hold_action: optional(actionConfigStruct), - double_tap_action: optional(actionConfigStruct), + type: literal("entity"), + entity: optional(string()), + name: optional(string()), + content_info: optional(string()), + icon: optional(string()), + icon_color: optional(string()), + use_entity_picture: optional(boolean()), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + double_tap_action: optional(actionConfigStruct), }); const menuChipConfigStruct = object({ - type: literal("menu"), - icon: optional(string()), - icon_color: optional(string()), + type: literal("menu"), + icon: optional(string()), + icon_color: optional(string()), }); const weatherChipConfigStruct = object({ - type: literal("weather"), - entity: optional(string()), - tap_action: optional(actionConfigStruct), - hold_action: optional(actionConfigStruct), - double_tap_action: optional(actionConfigStruct), - show_temperature: optional(boolean()), - show_conditions: optional(boolean()), + type: literal("weather"), + entity: optional(string()), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + double_tap_action: optional(actionConfigStruct), + show_temperature: optional(boolean()), + show_conditions: optional(boolean()), }); const conditionChipConfigStruct = object({ - type: literal("conditional"), - chip: optional(any()), - conditions: optional(array(any())), + type: literal("conditional"), + chip: optional(any()), + conditions: optional(array(any())), }); const lightChipConfigStruct = object({ - type: literal("light"), - entity: optional(string()), - name: optional(string()), - content_info: optional(string()), - icon: optional(string()), - use_light_color: optional(boolean()), - tap_action: optional(actionConfigStruct), - hold_action: optional(actionConfigStruct), - double_tap_action: optional(actionConfigStruct), + type: literal("light"), + entity: optional(string()), + name: optional(string()), + content_info: optional(string()), + icon: optional(string()), + use_light_color: optional(boolean()), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + double_tap_action: optional(actionConfigStruct), }); const templateChipConfigStruct = object({ - type: literal("template"), - entity: optional(string()), - tap_action: optional(actionConfigStruct), - hold_action: optional(actionConfigStruct), - double_tap_action: optional(actionConfigStruct), - content: optional(string()), - icon: optional(string()), - icon_color: optional(string()), - picture: optional(string()), - entity_id: optional(union([string(), array(string())])), + type: literal("template"), + entity: optional(string()), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + double_tap_action: optional(actionConfigStruct), + content: optional(string()), + icon: optional(string()), + icon_color: optional(string()), + picture: optional(string()), + entity_id: optional(union([string(), array(string())])), }); const spacerChipConfigStruct = object({ - type: literal("spacer"), + type: literal("spacer"), }); const chipsConfigStruct = dynamic((value) => { - if (value && typeof value === "object" && "type" in value) { - switch ((value as LovelaceChipConfig).type!) { - case "action": - return actionChipConfigStruct; - case "back": - return backChipConfigStruct; - case "entity": - return entityChipConfigStruct; - case "menu": - return menuChipConfigStruct; - case "weather": - return weatherChipConfigStruct; - case "conditional": - return conditionChipConfigStruct; - case "light": - return lightChipConfigStruct; - case "template": - return templateChipConfigStruct; - case "spacer": - return spacerChipConfigStruct; - } + if (value && typeof value === "object" && "type" in value) { + switch ((value as LovelaceChipConfig).type!) { + case "action": + return actionChipConfigStruct; + case "back": + return backChipConfigStruct; + case "entity": + return entityChipConfigStruct; + case "menu": + return menuChipConfigStruct; + case "weather": + return weatherChipConfigStruct; + case "conditional": + return conditionChipConfigStruct; + case "light": + return lightChipConfigStruct; + case "template": + return templateChipConfigStruct; + case "spacer": + return spacerChipConfigStruct; } - return object(); + } + return object(); }); const cardConfigStruct = assign( - lovelaceCardConfigStruct, - object({ - chips: array(chipsConfigStruct), - alignment: optional(string()), - }) + lovelaceCardConfigStruct, + object({ + chips: array(chipsConfigStruct), + alignment: optional(string()), + }) ); @customElement(CHIPS_CARD_EDITOR_NAME) -export class ChipsCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: ChipsCardConfig; - - @state() private _subElementEditorConfig?: SubElementEditorConfig; - - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } - - public setConfig(config: ChipsCardConfig): void { - assert(config, cardConfigStruct); - this._config = config; +export class ChipsCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: ChipsCardConfig; + + @state() private _subElementEditorConfig?: SubElementEditorConfig; + + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } + + public setConfig(config: ChipsCardConfig): void { + assert(config, cardConfigStruct); + this._config = config; + } + + get _title(): string { + return this._config!.title || ""; + } + + get _theme(): string { + return this._config!.theme || ""; + } + + protected render() { + if (!this.hass || !this._config) { + return nothing; } - get _title(): string { - return this._config!.title || ""; + if (this._subElementEditorConfig) { + return html` + + + `; } - get _theme(): string { - return this._config!.theme || ""; + const customLocalize = setupCustomlocalize(this.hass); + + return html` +
+ + +
+ + `; + } + + private _valueChanged(ev: CustomEvent): void { + if (!this._config || !this.hass) { + return; } - - protected render() { - if (!this.hass || !this._config) { - return nothing; - } - - if (this._subElementEditorConfig) { - return html` - - - `; + const target = ev.target! as EditorTarget; + const configValue = + target.configValue || this._subElementEditorConfig?.type; + const value = target.checked ?? ev.detail.value ?? target.value; + + if (configValue === "chip" || (ev.detail && ev.detail.chips)) { + const newConfigChips = ev.detail.chips || this._config!.chips.concat(); + if (configValue === "chip") { + if (!value) { + newConfigChips.splice(this._subElementEditorConfig!.index!, 1); + this._goBack(); + } else { + newConfigChips[this._subElementEditorConfig!.index!] = value; } - const customLocalize = setupCustomlocalize(this.hass); - - return html` -
- - -
- - `; + this._subElementEditorConfig!.elementConfig = value; + } + + this._config = { ...this._config!, chips: newConfigChips }; + } else if (configValue) { + if (!value) { + this._config = { ...this._config }; + delete this._config[configValue!]; + } else { + this._config = { + ...this._config, + [configValue!]: value, + }; + } } - private _valueChanged(ev: CustomEvent): void { - if (!this._config || !this.hass) { - return; - } - const target = ev.target! as EditorTarget; - const configValue = target.configValue || this._subElementEditorConfig?.type; - const value = target.checked ?? ev.detail.value ?? target.value; - - if (configValue === "chip" || (ev.detail && ev.detail.chips)) { - const newConfigChips = ev.detail.chips || this._config!.chips.concat(); - if (configValue === "chip") { - if (!value) { - newConfigChips.splice(this._subElementEditorConfig!.index!, 1); - this._goBack(); - } else { - newConfigChips[this._subElementEditorConfig!.index!] = value; - } - - this._subElementEditorConfig!.elementConfig = value; - } - - this._config = { ...this._config!, chips: newConfigChips }; - } else if (configValue) { - if (!value) { - this._config = { ...this._config }; - delete this._config[configValue!]; - } else { - this._config = { - ...this._config, - [configValue!]: value, - }; - } - } + fireEvent(this, "config-changed", { config: this._config }); + } - fireEvent(this, "config-changed", { config: this._config }); + private _handleSubElementChanged(ev: CustomEvent): void { + ev.stopPropagation(); + if (!this._config || !this.hass) { + return; } - private _handleSubElementChanged(ev: CustomEvent): void { - ev.stopPropagation(); - if (!this._config || !this.hass) { - return; - } - - const configValue = this._subElementEditorConfig?.type; - const value = ev.detail.config; - - if (configValue === "chip") { - const newConfigChips = this._config!.chips!.concat(); - if (!value) { - newConfigChips.splice(this._subElementEditorConfig!.index!, 1); - this._goBack(); - } else { - newConfigChips[this._subElementEditorConfig!.index!] = value; - } - - this._config = { ...this._config!, chips: newConfigChips }; - } else if (configValue) { - if (value === "") { - this._config = { ...this._config }; - delete this._config[configValue!]; - } else { - this._config = { - ...this._config, - [configValue]: value, - }; - } - } - - this._subElementEditorConfig = { - ...this._subElementEditorConfig!, - elementConfig: value, + const configValue = this._subElementEditorConfig?.type; + const value = ev.detail.config; + + if (configValue === "chip") { + const newConfigChips = this._config!.chips!.concat(); + if (!value) { + newConfigChips.splice(this._subElementEditorConfig!.index!, 1); + this._goBack(); + } else { + newConfigChips[this._subElementEditorConfig!.index!] = value; + } + + this._config = { ...this._config!, chips: newConfigChips }; + } else if (configValue) { + if (value === "") { + this._config = { ...this._config }; + delete this._config[configValue!]; + } else { + this._config = { + ...this._config, + [configValue]: value, }; - - fireEvent(this, "config-changed", { config: this._config }); + } } - private _editDetailElement(ev: HASSDomEvent): void { - this._subElementEditorConfig = ev.detail.subElementConfig; - } + this._subElementEditorConfig = { + ...this._subElementEditorConfig!, + elementConfig: value, + }; - private _goBack(): void { - this._subElementEditorConfig = undefined; - } + fireEvent(this, "config-changed", { config: this._config }); + } + + private _editDetailElement(ev: HASSDomEvent): void { + this._subElementEditorConfig = ev.detail.subElementConfig; + } + + private _goBack(): void { + this._subElementEditorConfig = undefined; + } } diff --git a/src/cards/chips-card/chips-card.ts b/src/cards/chips-card/chips-card.ts index d8d60d9f5..e6a7e6be3 100644 --- a/src/cards/chips-card/chips-card.ts +++ b/src/cards/chips-card/chips-card.ts @@ -1,141 +1,146 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { - computeRTL, - HomeAssistant, - LovelaceCard, - LovelaceCardConfig, - LovelaceCardEditor, + computeRTL, + HomeAssistant, + LovelaceCard, + LovelaceCardConfig, + LovelaceCardEditor, } from "../../ha"; import "../../shared/chip"; import { computeDarkMode, MushroomBaseElement } from "../../utils/base-element"; import { registerCustomCard } from "../../utils/custom-cards"; import { createChipElement } from "../../utils/lovelace/chip/chip-element"; -import { LovelaceChip, LovelaceChipConfig } from "../../utils/lovelace/chip/types"; +import { + LovelaceChip, + LovelaceChipConfig, +} from "../../utils/lovelace/chip/types"; import "./chips"; import { setupConditionChipComponent } from "./chips/conditional-chip"; import { EntityChip } from "./chips/entity-chip"; import { CHIPS_CARD_EDITOR_NAME, CHIPS_CARD_NAME } from "./const"; export interface ChipsCardConfig extends LovelaceCardConfig { - chips: LovelaceChipConfig[]; - alignment?: string; + chips: LovelaceChipConfig[]; + alignment?: string; } registerCustomCard({ - type: CHIPS_CARD_NAME, - name: "Mushroom Chips Card", - description: "Card with chips to display informations", + type: CHIPS_CARD_NAME, + name: "Mushroom Chips Card", + description: "Card with chips to display informations", }); @customElement(CHIPS_CARD_NAME) export class ChipsCard extends LitElement implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./chips-card-editor"); - return document.createElement(CHIPS_CARD_EDITOR_NAME) as LovelaceCardEditor; + public static async getConfigElement(): Promise { + await import("./chips-card-editor"); + return document.createElement(CHIPS_CARD_EDITOR_NAME) as LovelaceCardEditor; + } + + public static async getStubConfig( + _hass: HomeAssistant + ): Promise { + const chips = await Promise.all([EntityChip.getStubConfig(_hass)]); + return { + type: `custom:${CHIPS_CARD_NAME}`, + chips, + }; + } + + @property() public preview?: boolean; + + @property() public editMode?: boolean; + + @state() private _config?: ChipsCardConfig; + + private _hass?: HomeAssistant; + + set hass(hass: HomeAssistant) { + const currentDarkMode = computeDarkMode(this._hass); + const newDarkMode = computeDarkMode(hass); + if (currentDarkMode !== newDarkMode) { + this.toggleAttribute("dark-mode", newDarkMode); } - - public static async getStubConfig(_hass: HomeAssistant): Promise { - const chips = await Promise.all([EntityChip.getStubConfig(_hass)]); - return { - type: `custom:${CHIPS_CARD_NAME}`, - chips, - }; + this._hass = hass; + this.shadowRoot?.querySelectorAll("div > *").forEach((element: unknown) => { + (element as LovelaceChip).hass = hass; + }); + } + + getCardSize(): number | Promise { + return 1; + } + + setConfig(config: ChipsCardConfig): void { + this._config = config; + } + + protected render() { + if (!this._config || !this._hass) { + return nothing; } - @property() public preview?: boolean; - - @property() public editMode?: boolean; + let alignment = ""; + if (this._config.alignment) { + alignment = `align-${this._config.alignment}`; + } - @state() private _config?: ChipsCardConfig; + const rtl = computeRTL(this._hass); - private _hass?: HomeAssistant; + return html` + +
+ ${this._config.chips.map((chip) => this.renderChip(chip))} +
+
+ `; + } - set hass(hass: HomeAssistant) { - const currentDarkMode = computeDarkMode(this._hass); - const newDarkMode = computeDarkMode(hass); - if (currentDarkMode !== newDarkMode) { - this.toggleAttribute("dark-mode", newDarkMode); - } - this._hass = hass; - this.shadowRoot?.querySelectorAll("div > *").forEach((element: unknown) => { - (element as LovelaceChip).hass = hass; - }); + private renderChip(chipConfig: LovelaceChipConfig) { + if (chipConfig.type === "conditional") { + setupConditionChipComponent(); } - - getCardSize(): number | Promise { - return 1; + const element = createChipElement(chipConfig); + if (!element) { + return nothing; } - - setConfig(config: ChipsCardConfig): void { - this._config = config; + if (this._hass) { + element.hass = this._hass; } - - protected render() { - if (!this._config || !this._hass) { - return nothing; + element.editMode = this.editMode || this.preview; + element.preview = this.preview || this.editMode; + return html`${element}`; + } + + static get styles(): CSSResultGroup { + return [ + MushroomBaseElement.styles, + css` + ha-card { + background: none; + box-shadow: none; + border-radius: 0; + border: none; } - - let alignment = ""; - if (this._config.alignment) { - alignment = `align-${this._config.alignment}`; + .chip-container { + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; + flex-wrap: wrap; + gap: var(--chip-spacing); } - - const rtl = computeRTL(this._hass); - - return html` - -
- ${this._config.chips.map((chip) => this.renderChip(chip))} -
-
- `; - } - - private renderChip(chipConfig: LovelaceChipConfig) { - if (chipConfig.type === "conditional") { - setupConditionChipComponent(); + .chip-container.align-end { + justify-content: flex-end; } - const element = createChipElement(chipConfig); - if (!element) { - return nothing; + .chip-container.align-center { + justify-content: center; } - if (this._hass) { - element.hass = this._hass; + .chip-container.align-justify { + justify-content: space-between; } - element.editMode = this.editMode || this.preview; - element.preview = this.preview || this.editMode; - return html`${element}`; - } - - static get styles(): CSSResultGroup { - return [ - MushroomBaseElement.styles, - css` - ha-card { - background: none; - box-shadow: none; - border-radius: 0; - border: none; - } - .chip-container { - display: flex; - flex-direction: row; - align-items: flex-start; - justify-content: flex-start; - flex-wrap: wrap; - gap: var(--chip-spacing); - } - .chip-container.align-end { - justify-content: flex-end; - } - .chip-container.align-center { - justify-content: center; - } - .chip-container.align-justify { - justify-content: space-between; - } - `, - ]; - } + `, + ]; + } } diff --git a/src/cards/chips-card/chips/action-chip-editor.ts b/src/cards/chips-card/chips/action-chip-editor.ts index f156120ab..1634251db 100644 --- a/src/cards/chips-card/chips/action-chip-editor.ts +++ b/src/cards/chips-card/chips/action-chip-editor.ts @@ -11,56 +11,67 @@ import { ActionChipConfig } from "../../../utils/lovelace/chip/types"; import { LovelaceChipEditor } from "../../../utils/lovelace/types"; import { DEFAULT_ACTION_ICON } from "./action-chip"; -const actions: UiAction[] = ["navigate", "url", "call-service", "assist", "none"]; +const actions: UiAction[] = [ + "navigate", + "url", + "call-service", + "assist", + "none", +]; const SCHEMA: HaFormSchema[] = [ - { - type: "grid", - name: "", - schema: [ - { name: "icon", selector: { icon: { placeholder: DEFAULT_ACTION_ICON } } }, - { name: "icon_color", selector: { mush_color: {} } }, - ], - }, - ...computeActionsFormSchema(actions), + { + type: "grid", + name: "", + schema: [ + { + name: "icon", + selector: { icon: { placeholder: DEFAULT_ACTION_ICON } }, + }, + { name: "icon_color", selector: { mush_color: {} } }, + ], + }, + ...computeActionsFormSchema(actions), ]; @customElement(computeChipEditorComponentName("action")) export class EntityChipEditor extends LitElement implements LovelaceChipEditor { - @property({ attribute: false }) public hass?: HomeAssistant; - - @state() private _config?: ActionChipConfig; - - public setConfig(config: ActionChipConfig): void { - this._config = config; - } + @property({ attribute: false }) public hass?: HomeAssistant; - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + @state() private _config?: ActionChipConfig; - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + public setConfig(config: ActionChipConfig): void { + this._config = config; + } - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - return html` - - `; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/chips-card/chips/action-chip.ts b/src/cards/chips-card/chips/action-chip.ts index cfb614222..0403e7671 100644 --- a/src/cards/chips-card/chips/action-chip.ts +++ b/src/cards/chips-card/chips/action-chip.ts @@ -2,92 +2,97 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - computeRTL, - handleAction, - hasAction, - HomeAssistant, + actionHandler, + ActionHandlerEvent, + computeRTL, + handleAction, + hasAction, + HomeAssistant, } from "../../../ha"; import { computeRgbColor } from "../../../utils/colors"; import { - computeChipComponentName, - computeChipEditorComponentName, + computeChipComponentName, + computeChipEditorComponentName, } from "../../../utils/lovelace/chip/chip-element"; -import { ActionChipConfig, LovelaceChip } from "../../../utils/lovelace/chip/types"; +import { + ActionChipConfig, + LovelaceChip, +} from "../../../utils/lovelace/chip/types"; import { LovelaceChipEditor } from "../../../utils/lovelace/types"; export const DEFAULT_ACTION_ICON = "mdi:flash"; @customElement(computeChipComponentName("action")) export class ActionChip extends LitElement implements LovelaceChip { - public static async getConfigElement(): Promise { - await import("./action-chip-editor"); - return document.createElement( - computeChipEditorComponentName("action") - ) as LovelaceChipEditor; - } + public static async getConfigElement(): Promise { + await import("./action-chip-editor"); + return document.createElement( + computeChipEditorComponentName("action") + ) as LovelaceChipEditor; + } - public static async getStubConfig(_hass: HomeAssistant): Promise { - return { - type: `action`, - }; - } + public static async getStubConfig( + _hass: HomeAssistant + ): Promise { + return { + type: `action`, + }; + } - @property({ attribute: false }) public hass?: HomeAssistant; + @property({ attribute: false }) public hass?: HomeAssistant; - @state() private _config?: ActionChipConfig; + @state() private _config?: ActionChipConfig; - public setConfig(config: ActionChipConfig): void { - this._config = config; - } + public setConfig(config: ActionChipConfig): void { + this._config = config; + } - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); - } + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + protected render() { + if (!this.hass || !this._config) { + return nothing; + } - const icon = this._config.icon || DEFAULT_ACTION_ICON; - const iconColor = this._config.icon_color; + const icon = this._config.icon || DEFAULT_ACTION_ICON; + const iconColor = this._config.icon_color; - const iconStyle = {}; - if (iconColor) { - const iconRgbColor = computeRgbColor(iconColor); - iconStyle["--color"] = `rgb(${iconRgbColor})`; - } + const iconStyle = {}; + if (iconColor) { + const iconRgbColor = computeRgbColor(iconColor); + iconStyle["--color"] = `rgb(${iconRgbColor})`; + } - const rtl = computeRTL(this.hass); + const rtl = computeRTL(this.hass); - return html` - - - - `; - } + return html` + + + + `; + } - static get styles(): CSSResultGroup { - return css` - mushroom-chip { - cursor: pointer; - } - ha-state-icon { - color: var(--color); - } - `; - } + static get styles(): CSSResultGroup { + return css` + mushroom-chip { + cursor: pointer; + } + ha-state-icon { + color: var(--color); + } + `; + } } diff --git a/src/cards/chips-card/chips/alarm-control-panel-chip-editor.ts b/src/cards/chips-card/chips/alarm-control-panel-chip-editor.ts index 458d3e54a..f3c155ee4 100644 --- a/src/cards/chips-card/chips/alarm-control-panel-chip-editor.ts +++ b/src/cards/chips-card/chips/alarm-control-panel-chip-editor.ts @@ -11,58 +11,73 @@ import { AlarmControlPanelChipConfig } from "../../../utils/lovelace/chip/types" import { LovelaceChipEditor } from "../../../utils/lovelace/types"; import { ALARM_CONTROl_PANEL_ENTITY_DOMAINS } from "../../alarm-control-panel-card/const"; -const actions: UiAction[] = ["more-info", "navigate", "url", "call-service", "assist", "none"]; +const actions: UiAction[] = [ + "more-info", + "navigate", + "url", + "call-service", + "assist", + "none", +]; const SCHEMA: HaFormSchema[] = [ - { name: "entity", selector: { entity: { domain: ALARM_CONTROl_PANEL_ENTITY_DOMAINS } } }, - { - type: "grid", - name: "", - schema: [ - { name: "name", selector: { text: {} } }, - { name: "content_info", selector: { mush_info: {} } }, - ], - }, - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - ...computeActionsFormSchema(actions), + { + name: "entity", + selector: { entity: { domain: ALARM_CONTROl_PANEL_ENTITY_DOMAINS } }, + }, + { + type: "grid", + name: "", + schema: [ + { name: "name", selector: { text: {} } }, + { name: "content_info", selector: { mush_info: {} } }, + ], + }, + { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, + ...computeActionsFormSchema(actions), ]; @customElement(computeChipEditorComponentName("alarm-control-panel")) -export class AlarmControlPanelChipEditor extends LitElement implements LovelaceChipEditor { - @property({ attribute: false }) public hass?: HomeAssistant; - - @state() private _config?: AlarmControlPanelChipConfig; - - public setConfig(config: AlarmControlPanelChipConfig): void { - this._config = config; - } +export class AlarmControlPanelChipEditor + extends LitElement + implements LovelaceChipEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + @state() private _config?: AlarmControlPanelChipConfig; - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + public setConfig(config: AlarmControlPanelChipConfig): void { + this._config = config; + } - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - return html` - - `; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/chips-card/chips/alarm-control-panel-chip.ts b/src/cards/chips-card/chips/alarm-control-panel-chip.ts index 195411639..8638c56b6 100644 --- a/src/cards/chips-card/chips/alarm-control-panel-chip.ts +++ b/src/cards/chips-card/chips/alarm-control-panel-chip.ts @@ -4,139 +4,144 @@ import { customElement, property, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - computeRTL, - computeStateDisplay, - handleAction, - hasAction, - HomeAssistant, + actionHandler, + ActionHandlerEvent, + computeRTL, + computeStateDisplay, + handleAction, + hasAction, + HomeAssistant, } from "../../../ha"; import { computeRgbColor } from "../../../utils/colors"; import { animation } from "../../../utils/entity-styles"; import { computeInfoDisplay } from "../../../utils/info"; import { - computeChipComponentName, - computeChipEditorComponentName, + computeChipComponentName, + computeChipEditorComponentName, } from "../../../utils/lovelace/chip/chip-element"; import { - AlarmControlPanelChipConfig, - EntityChipConfig, - LovelaceChip, + AlarmControlPanelChipConfig, + EntityChipConfig, + LovelaceChip, } from "../../../utils/lovelace/chip/types"; import { LovelaceChipEditor } from "../../../utils/lovelace/types"; import { ALARM_CONTROl_PANEL_ENTITY_DOMAINS } from "../../alarm-control-panel-card/const"; -import { getStateColor, shouldPulse } from "../../alarm-control-panel-card/utils"; +import { + getStateColor, + shouldPulse, +} from "../../alarm-control-panel-card/utils"; @customElement(computeChipComponentName("alarm-control-panel")) export class AlarmControlPanelChip extends LitElement implements LovelaceChip { - public static async getConfigElement(): Promise { - await import("./alarm-control-panel-chip-editor"); - return document.createElement( - computeChipEditorComponentName("alarm-control-panel") - ) as LovelaceChipEditor; - } - - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - const panels = entities.filter((e) => - ALARM_CONTROl_PANEL_ENTITY_DOMAINS.includes(e.split(".")[0]) - ); - return { - type: `alarm-control-panel`, - entity: panels[0], - }; + public static async getConfigElement(): Promise { + await import("./alarm-control-panel-chip-editor"); + return document.createElement( + computeChipEditorComponentName("alarm-control-panel") + ) as LovelaceChipEditor; + } + + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + const panels = entities.filter((e) => + ALARM_CONTROl_PANEL_ENTITY_DOMAINS.includes(e.split(".")[0]) + ); + return { + type: `alarm-control-panel`, + entity: panels[0], + }; + } + + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: EntityChipConfig; + + public setConfig(config: EntityChipConfig): void { + this._config = config; + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + protected render() { + if (!this.hass || !this._config || !this._config.entity) { + return nothing; } - @property({ attribute: false }) public hass?: HomeAssistant; + const entityId = this._config.entity; + const stateObj = this.hass.states[entityId] as HassEntity | undefined; - @state() private _config?: EntityChipConfig; - - public setConfig(config: EntityChipConfig): void { - this._config = config; - } - - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); + if (!stateObj) { + return nothing; } - protected render() { - if (!this.hass || !this._config || !this._config.entity) { - return nothing; - } - - const entityId = this._config.entity; - const stateObj = this.hass.states[entityId] as HassEntity | undefined; - - if (!stateObj) { - return nothing; - } - - const name = this._config.name || stateObj.attributes.friendly_name || ""; - const icon = this._config.icon; - const iconColor = getStateColor(stateObj.state); - const iconPulse = shouldPulse(stateObj.state); - - const stateDisplay = this.hass.formatEntityState - ? this.hass.formatEntityState(stateObj) - : computeStateDisplay( - this.hass.localize, - stateObj, - this.hass.locale, - this.hass.config, - this.hass.entities - ); - - const iconStyle = {}; - if (iconColor) { - const iconRgbColor = computeRgbColor(iconColor); - iconStyle["--color"] = `rgb(${iconRgbColor})`; - } - - const content = computeInfoDisplay( - this._config.content_info ?? "state", - name, - stateDisplay, - stateObj, - this.hass + const name = this._config.name || stateObj.attributes.friendly_name || ""; + const icon = this._config.icon; + const iconColor = getStateColor(stateObj.state); + const iconPulse = shouldPulse(stateObj.state); + + const stateDisplay = this.hass.formatEntityState + ? this.hass.formatEntityState(stateObj) + : computeStateDisplay( + this.hass.localize, + stateObj, + this.hass.locale, + this.hass.config, + this.hass.entities ); - const rtl = computeRTL(this.hass); - - return html` - - - ${content ? html`${content}` : nothing} - - `; + const iconStyle = {}; + if (iconColor) { + const iconRgbColor = computeRgbColor(iconColor); + iconStyle["--color"] = `rgb(${iconRgbColor})`; } - // Animation cannot be defined on chip element, key-frames cannot be scoped to a slotted element: https://github.com/WICG/webcomponents/issues/748 - static get styles(): CSSResultGroup { - return css` - mushroom-chip { - cursor: pointer; - } - ha-state-icon { - color: var(--color); - } - ha-state-icon.pulse { - animation: 1s ease 0s infinite normal none running pulse; - } - ${animation.pulse} - `; - } + const content = computeInfoDisplay( + this._config.content_info ?? "state", + name, + stateDisplay, + stateObj, + this.hass + ); + + const rtl = computeRTL(this.hass); + + return html` + + + ${content ? html`${content}` : nothing} + + `; + } + + // Animation cannot be defined on chip element, key-frames cannot be scoped to a slotted element: https://github.com/WICG/webcomponents/issues/748 + static get styles(): CSSResultGroup { + return css` + mushroom-chip { + cursor: pointer; + } + ha-state-icon { + color: var(--color); + } + ha-state-icon.pulse { + animation: 1s ease 0s infinite normal none running pulse; + } + ${animation.pulse} + `; + } } diff --git a/src/cards/chips-card/chips/back-chip-editor.ts b/src/cards/chips-card/chips/back-chip-editor.ts index e07196221..5afae55aa 100644 --- a/src/cards/chips-card/chips/back-chip-editor.ts +++ b/src/cards/chips-card/chips/back-chip-editor.ts @@ -8,40 +8,42 @@ import { LovelaceChipEditor } from "../../../utils/lovelace/types"; import { DEFAULT_BACK_ICON } from "./back-chip"; const SCHEMA: HaFormSchema[] = [ - { name: "icon", selector: { icon: { placeholder: DEFAULT_BACK_ICON } } }, + { name: "icon", selector: { icon: { placeholder: DEFAULT_BACK_ICON } } }, ]; @customElement(computeChipEditorComponentName("back")) export class BackChipEditor extends LitElement implements LovelaceChipEditor { - @property({ attribute: false }) public hass?: HomeAssistant; + @property({ attribute: false }) public hass?: HomeAssistant; - @state() private _config?: EntityChipConfig; + @state() private _config?: EntityChipConfig; - public setConfig(config: EntityChipConfig): void { - this._config = config; - } + public setConfig(config: EntityChipConfig): void { + this._config = config; + } - private _computeLabel = (schema: HaFormSchema) => { - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; - - protected render() { - if (!this.hass || !this._config) { - return nothing; - } - - return html` - - `; - } + private _computeLabel = (schema: HaFormSchema) => { + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/chips-card/chips/back-chip.ts b/src/cards/chips-card/chips/back-chip.ts index 880d58dd5..5f00e427a 100644 --- a/src/cards/chips-card/chips/back-chip.ts +++ b/src/cards/chips-card/chips/back-chip.ts @@ -2,64 +2,71 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { actionHandler, computeRTL, HomeAssistant } from "../../../ha"; import { - computeChipComponentName, - computeChipEditorComponentName, + computeChipComponentName, + computeChipEditorComponentName, } from "../../../utils/lovelace/chip/chip-element"; -import { BackChipConfig, LovelaceChip } from "../../../utils/lovelace/chip/types"; +import { + BackChipConfig, + LovelaceChip, +} from "../../../utils/lovelace/chip/types"; import { LovelaceChipEditor } from "../../../utils/lovelace/types"; export const DEFAULT_BACK_ICON = "mdi:arrow-left"; @customElement(computeChipComponentName("back")) export class BackChip extends LitElement implements LovelaceChip { - public static async getConfigElement(): Promise { - await import("./back-chip-editor"); - return document.createElement(computeChipEditorComponentName("back")) as LovelaceChipEditor; - } + public static async getConfigElement(): Promise { + await import("./back-chip-editor"); + return document.createElement( + computeChipEditorComponentName("back") + ) as LovelaceChipEditor; + } - public static async getStubConfig(_hass: HomeAssistant): Promise { - return { - type: `back`, - }; - } + public static async getStubConfig( + _hass: HomeAssistant + ): Promise { + return { + type: `back`, + }; + } - @property({ attribute: false }) public hass?: HomeAssistant; + @property({ attribute: false }) public hass?: HomeAssistant; - @state() private _config?: BackChipConfig; + @state() private _config?: BackChipConfig; - public setConfig(config: BackChipConfig): void { - this._config = config; - } + public setConfig(config: BackChipConfig): void { + this._config = config; + } - private _handleAction() { - window.history.back(); - } + private _handleAction() { + window.history.back(); + } - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + protected render() { + if (!this.hass || !this._config) { + return nothing; + } - const icon = this._config.icon || DEFAULT_BACK_ICON; + const icon = this._config.icon || DEFAULT_BACK_ICON; - const rtl = computeRTL(this.hass); + const rtl = computeRTL(this.hass); - return html` - - - - `; - } + return html` + + + + `; + } - static get styles(): CSSResultGroup { - return css` - mushroom-chip { - cursor: pointer; - } - `; - } + static get styles(): CSSResultGroup { + return css` + mushroom-chip { + cursor: pointer; + } + `; + } } diff --git a/src/cards/chips-card/chips/conditional-chip-editor.ts b/src/cards/chips-card/chips/conditional-chip-editor.ts index 9cc5cfaaa..4236b097e 100644 --- a/src/cards/chips-card/chips/conditional-chip-editor.ts +++ b/src/cards/chips-card/chips/conditional-chip-editor.ts @@ -1,7 +1,12 @@ import type { MDCTabBarActivatedEvent } from "@material/tab-bar"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; -import { fireEvent, HASSDomEvent, HomeAssistant, LovelaceConfig } from "../../../ha"; +import { + fireEvent, + HASSDomEvent, + HomeAssistant, + LovelaceConfig, +} from "../../../ha"; import setupCustomlocalize from "../../../localize"; import "../../../shared/form/mushroom-select"; import "../../../shared/form/mushroom-textfield"; @@ -9,235 +14,240 @@ import { loadHaComponents } from "../../../utils/loader"; import { getChipElementClass } from "../../../utils/lovelace/chip-element-editor"; import { computeChipEditorComponentName } from "../../../utils/lovelace/chip/chip-element"; import { - CHIP_LIST, - ConditionalChipConfig, - LovelaceChipConfig, + CHIP_LIST, + ConditionalChipConfig, + LovelaceChipConfig, } from "../../../utils/lovelace/chip/types"; import { GUIModeChangedEvent } from "../../../utils/lovelace/editor/types"; import { ConfigChangedEvent } from "../../../utils/lovelace/element-editor"; import { LovelaceChipEditor } from "../../../utils/lovelace/types"; @customElement(computeChipEditorComponentName("conditional")) -export class ConditionalChipEditor extends LitElement implements LovelaceChipEditor { - @property({ attribute: false }) public hass?: HomeAssistant; +export class ConditionalChipEditor + extends LitElement + implements LovelaceChipEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public lovelace?: LovelaceConfig; + @property({ attribute: false }) public lovelace?: LovelaceConfig; - @state() private _config?: ConditionalChipConfig; + @state() private _config?: ConditionalChipConfig; - @state() private _GUImode = true; + @state() private _GUImode = true; - @state() private _guiModeAvailable? = true; + @state() private _guiModeAvailable? = true; - @state() private _cardTab = false; + @state() private _cardTab = false; - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } - @query("mushroom-chip-element-editor") - private _cardEditorEl?: any; + @query("mushroom-chip-element-editor") + private _cardEditorEl?: any; - public setConfig(config: ConditionalChipConfig): void { - this._config = config; - } + public setConfig(config: ConditionalChipConfig): void { + this._config = config; + } - public focusYamlEditor() { - this._cardEditorEl?.focusYamlEditor(); - } + public focusYamlEditor() { + this._cardEditorEl?.focusYamlEditor(); + } - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + protected render() { + if (!this.hass || !this._config) { + return nothing; + } - const customLocalize = setupCustomlocalize(this.hass); - - return html` - - - - - ${this._cardTab + const customLocalize = setupCustomlocalize(this.hass); + + return html` + + + + + ${this._cardTab + ? html` +
+ ${this._config.chip?.type !== undefined ? html` -
- ${this._config.chip?.type !== undefined - ? html` -
- - ${this.hass!.localize( - !this._cardEditorEl || this._GUImode - ? "ui.panel.lovelace.editor.edit_card.show_code_editor" - : "ui.panel.lovelace.editor.edit_card.show_visual_editor" - )} - - ${this.hass!.localize( - "ui.panel.lovelace.editor.card.conditional.change_type" - )} -
- - ` - : html` - e.stopPropagation()} - fixedMenuPosition - naturalMenuWidth - > - ${CHIP_LIST.map( - (chip) => html` - - ${customLocalize( - `editor.chip.chip-picker.types.${chip}` - )} - - ` - )} - - `} -
+
+ + ${this.hass!.localize( + !this._cardEditorEl || this._GUImode + ? "ui.panel.lovelace.editor.edit_card.show_code_editor" + : "ui.panel.lovelace.editor.edit_card.show_visual_editor" + )} + + ${this.hass!.localize( + "ui.panel.lovelace.editor.card.conditional.change_type" + )} +
+ ` : html` - + e.stopPropagation()} + fixedMenuPosition + naturalMenuWidth + > + ${CHIP_LIST.map( + (chip) => html` + + ${customLocalize( + `editor.chip.chip-picker.types.${chip}` + )} + + ` + )} + `} - `; +
+ ` + : html` + + `} + `; + } + + private _selectTab(ev: MDCTabBarActivatedEvent): void { + this._cardTab = ev.detail.index === 1; + } + + private _toggleMode(): void { + this._cardEditorEl?.toggleMode(); + } + + private _setMode(value: boolean): void { + this._GUImode = value; + if (this._cardEditorEl) { + this._cardEditorEl.GUImode = value; } + } - private _selectTab(ev: MDCTabBarActivatedEvent): void { - this._cardTab = ev.detail.index === 1; - } + private _handleGUIModeChanged(ev: HASSDomEvent): void { + ev.stopPropagation(); + this._GUImode = ev.detail.guiMode; + this._guiModeAvailable = ev.detail.guiModeAvailable; + } - private _toggleMode(): void { - this._cardEditorEl?.toggleMode(); - } - - private _setMode(value: boolean): void { - this._GUImode = value; - if (this._cardEditorEl) { - this._cardEditorEl.GUImode = value; - } - } + private async _handleChipPicked(ev: CustomEvent): Promise { + const value = (ev.target as any).value; - private _handleGUIModeChanged(ev: HASSDomEvent): void { - ev.stopPropagation(); - this._GUImode = ev.detail.guiMode; - this._guiModeAvailable = ev.detail.guiModeAvailable; + if (value === "") { + return; } - private async _handleChipPicked(ev: CustomEvent): Promise { - const value = (ev.target as any).value; - - if (value === "") { - return; - } - - let newChip: LovelaceChipConfig; - - const elClass = getChipElementClass(value) as any; - - if (elClass && elClass.getStubConfig) { - newChip = (await elClass.getStubConfig(this.hass)) as LovelaceChipConfig; - } else { - newChip = { type: value }; - } + let newChip: LovelaceChipConfig; - (ev.target as any).value = ""; + const elClass = getChipElementClass(value) as any; - ev.stopPropagation(); - if (!this._config) { - return; - } - this._setMode(true); - this._guiModeAvailable = true; - this._config = { ...this._config, chip: newChip }; - fireEvent(this, "config-changed", { config: this._config }); + if (elClass && elClass.getStubConfig) { + newChip = (await elClass.getStubConfig(this.hass)) as LovelaceChipConfig; + } else { + newChip = { type: value }; } - private _handleChipChanged(ev: HASSDomEvent): void { - ev.stopPropagation(); - if (!this._config) { - return; - } - this._config = { - ...this._config, - chip: ev.detail.config as LovelaceChipConfig, - }; - this._guiModeAvailable = ev.detail.guiModeAvailable; - fireEvent(this, "config-changed", { config: this._config }); - } + (ev.target as any).value = ""; - private _handleReplaceChip(): void { - if (!this._config) { - return; - } - // @ts-ignore - this._config = { ...this._config, chip: undefined }; - // @ts-ignore - fireEvent(this, "config-changed", { config: this._config }); + ev.stopPropagation(); + if (!this._config) { + return; } - - private _conditionChanged(ev: CustomEvent) { - ev.stopPropagation(); - if (!this._config) { - return; - } - const conditions = ev.detail.value; - this._config = { ...this._config, conditions }; - fireEvent(this, "config-changed", { config: this._config }); + this._setMode(true); + this._guiModeAvailable = true; + this._config = { ...this._config, chip: newChip }; + fireEvent(this, "config-changed", { config: this._config }); + } + + private _handleChipChanged(ev: HASSDomEvent): void { + ev.stopPropagation(); + if (!this._config) { + return; } - - static get styles(): CSSResultGroup { - return css` - mwc-tab-bar { - border-bottom: 1px solid var(--divider-color); - } - .card { - margin-top: 8px; - border: 1px solid var(--divider-color); - padding: 12px; - } - .card mushroom-select { - width: 100%; - margin-top: 0px; - } - @media (max-width: 450px) { - .card { - margin: 8px -12px 0; - } - } - .card .card-options { - display: flex; - justify-content: flex-end; - width: 100%; - } - .gui-mode-button { - margin-right: auto; - } - `; + this._config = { + ...this._config, + chip: ev.detail.config as LovelaceChipConfig, + }; + this._guiModeAvailable = ev.detail.guiModeAvailable; + fireEvent(this, "config-changed", { config: this._config }); + } + + private _handleReplaceChip(): void { + if (!this._config) { + return; + } + // @ts-ignore + this._config = { ...this._config, chip: undefined }; + // @ts-ignore + fireEvent(this, "config-changed", { config: this._config }); + } + + private _conditionChanged(ev: CustomEvent) { + ev.stopPropagation(); + if (!this._config) { + return; } + const conditions = ev.detail.value; + this._config = { ...this._config, conditions }; + fireEvent(this, "config-changed", { config: this._config }); + } + + static get styles(): CSSResultGroup { + return css` + mwc-tab-bar { + border-bottom: 1px solid var(--divider-color); + } + .card { + margin-top: 8px; + border: 1px solid var(--divider-color); + padding: 12px; + } + .card mushroom-select { + width: 100%; + margin-top: 0px; + } + @media (max-width: 450px) { + .card { + margin: 8px -12px 0; + } + } + .card .card-options { + display: flex; + justify-content: flex-end; + width: 100%; + } + .gui-mode-button { + margin-right: auto; + } + `; + } } diff --git a/src/cards/chips-card/chips/conditional-chip.ts b/src/cards/chips-card/chips/conditional-chip.ts index 6eb113045..50a8fb0ba 100644 --- a/src/cards/chips-card/chips/conditional-chip.ts +++ b/src/cards/chips-card/chips/conditional-chip.ts @@ -1,60 +1,63 @@ import { loadCustomElement } from "../../../utils/loader"; import { - computeChipComponentName, - computeChipEditorComponentName, - createChipElement, + computeChipComponentName, + computeChipEditorComponentName, + createChipElement, } from "../../../utils/lovelace/chip/chip-element"; -import { ConditionalChipConfig, LovelaceChip } from "../../../utils/lovelace/chip/types"; +import { + ConditionalChipConfig, + LovelaceChip, +} from "../../../utils/lovelace/chip/types"; import { LovelaceChipEditor } from "../../../utils/lovelace/types"; const componentName = computeChipComponentName("conditional"); export const setupConditionChipComponent = async () => { - // Don't resetup the component if already set up. - if (customElements.get(componentName)) { - return; + // Don't resetup the component if already set up. + if (customElements.get(componentName)) { + return; + } + + // Load conditional base + if (!customElements.get("hui-conditional-base")) { + const helpers = await (window as any).loadCardHelpers(); + helpers.createCardElement({ + type: "conditional", + card: { type: "button" }, + conditions: [], + }); + } + const HuiConditionalBase = await loadCustomElement("hui-conditional-base"); + + // @ts-ignore + class ConditionalChip extends HuiConditionalBase implements LovelaceChip { + public static async getConfigElement(): Promise { + await import("./conditional-chip-editor"); + return document.createElement( + computeChipEditorComponentName("conditional") + ) as LovelaceChipEditor; } - // Load conditional base - if (!customElements.get("hui-conditional-base")) { - const helpers = await (window as any).loadCardHelpers(); - helpers.createCardElement({ - type: "conditional", - card: { type: "button" }, - conditions: [], - }); + public static async getStubConfig(): Promise { + return { + type: `conditional`, + conditions: [], + }; } - const HuiConditionalBase = await loadCustomElement("hui-conditional-base"); - // @ts-ignore - class ConditionalChip extends HuiConditionalBase implements LovelaceChip { - public static async getConfigElement(): Promise { - await import("./conditional-chip-editor"); - return document.createElement( - computeChipEditorComponentName("conditional") - ) as LovelaceChipEditor; - } - - public static async getStubConfig(): Promise { - return { - type: `conditional`, - conditions: [], - }; - } - - public setConfig(config: ConditionalChipConfig): void { - this.validateConfig(config); - - if (!config.chip) { - throw new Error("No chip configured"); - } - - this._element = createChipElement(config.chip) as LovelaceChip; - } - } + public setConfig(config: ConditionalChipConfig): void { + this.validateConfig(config); - if (!customElements.get(componentName)) { - // @ts-ignore - customElements.define(componentName, ConditionalChip); + if (!config.chip) { + throw new Error("No chip configured"); + } + + this._element = createChipElement(config.chip) as LovelaceChip; } + } + + if (!customElements.get(componentName)) { + // @ts-ignore + customElements.define(componentName, ConditionalChip); + } }; diff --git a/src/cards/chips-card/chips/entity-chip-editor.ts b/src/cards/chips-card/chips/entity-chip-editor.ts index d7ecea3da..664fa94c3 100644 --- a/src/cards/chips-card/chips/entity-chip-editor.ts +++ b/src/cards/chips-card/chips/entity-chip-editor.ts @@ -10,63 +10,69 @@ import { EntityChipConfig } from "../../../utils/lovelace/chip/types"; import { LovelaceChipEditor } from "../../../utils/lovelace/types"; const SCHEMA: HaFormSchema[] = [ - { name: "entity", selector: { entity: {} } }, - { - type: "grid", - name: "", - schema: [ - { name: "name", selector: { text: {} } }, - { name: "content_info", selector: { mush_info: {} } }, - ], - }, - { - type: "grid", - name: "", - schema: [ - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - { name: "icon_color", selector: { mush_color: {} } }, - ], - }, - { name: "use_entity_picture", selector: { boolean: {} } }, - ...computeActionsFormSchema(), + { name: "entity", selector: { entity: {} } }, + { + type: "grid", + name: "", + schema: [ + { name: "name", selector: { text: {} } }, + { name: "content_info", selector: { mush_info: {} } }, + ], + }, + { + type: "grid", + name: "", + schema: [ + { + name: "icon", + selector: { icon: {} }, + context: { icon_entity: "entity" }, + }, + { name: "icon_color", selector: { mush_color: {} } }, + ], + }, + { name: "use_entity_picture", selector: { boolean: {} } }, + ...computeActionsFormSchema(), ]; @customElement(computeChipEditorComponentName("entity")) export class EntityChipEditor extends LitElement implements LovelaceChipEditor { - @property({ attribute: false }) public hass?: HomeAssistant; + @property({ attribute: false }) public hass?: HomeAssistant; - @state() private _config?: EntityChipConfig; + @state() private _config?: EntityChipConfig; - public setConfig(config: EntityChipConfig): void { - this._config = config; - } - - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); - - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + public setConfig(config: EntityChipConfig): void { + this._config = config; + } - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - return html` - - `; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/chips-card/chips/entity-chip.ts b/src/cards/chips-card/chips/entity-chip.ts index 49aae518b..f894fe571 100644 --- a/src/cards/chips-card/chips/entity-chip.ts +++ b/src/cards/chips-card/chips/entity-chip.ts @@ -1,144 +1,160 @@ -import { css, CSSResultGroup, html, LitElement, nothing, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + nothing, + TemplateResult, +} from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - computeRTL, - computeStateDisplay, - getEntityPicture, - handleAction, - hasAction, - HomeAssistant, - isActive, + actionHandler, + ActionHandlerEvent, + computeRTL, + computeStateDisplay, + getEntityPicture, + handleAction, + hasAction, + HomeAssistant, + isActive, } from "../../../ha"; import { computeRgbColor } from "../../../utils/colors"; import { computeInfoDisplay } from "../../../utils/info"; import { - computeChipComponentName, - computeChipEditorComponentName, + computeChipComponentName, + computeChipEditorComponentName, } from "../../../utils/lovelace/chip/chip-element"; -import { EntityChipConfig, LovelaceChip } from "../../../utils/lovelace/chip/types"; +import { + EntityChipConfig, + LovelaceChip, +} from "../../../utils/lovelace/chip/types"; import { LovelaceChipEditor } from "../../../utils/lovelace/types"; import { HassEntity } from "home-assistant-js-websocket"; @customElement(computeChipComponentName("entity")) export class EntityChip extends LitElement implements LovelaceChip { - public static async getConfigElement(): Promise { - await import("./entity-chip-editor"); - return document.createElement( - computeChipEditorComponentName("entity") - ) as LovelaceChipEditor; - } - - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - return { - type: `entity`, - entity: entities[0], - }; + public static async getConfigElement(): Promise { + await import("./entity-chip-editor"); + return document.createElement( + computeChipEditorComponentName("entity") + ) as LovelaceChipEditor; + } + + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + return { + type: `entity`, + entity: entities[0], + }; + } + + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: EntityChipConfig; + + public setConfig(config: EntityChipConfig): void { + this._config = config; + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + protected render() { + if (!this.hass || !this._config || !this._config.entity) { + return nothing; } - @property({ attribute: false }) public hass?: HomeAssistant; - - @state() private _config?: EntityChipConfig; + const entityId = this._config.entity; + const stateObj = this.hass.states[entityId] as HassEntity | undefined; - public setConfig(config: EntityChipConfig): void { - this._config = config; + if (!stateObj) { + return nothing; } - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); - } - - protected render() { - if (!this.hass || !this._config || !this._config.entity) { - return nothing; - } - - const entityId = this._config.entity; - const stateObj = this.hass.states[entityId] as HassEntity | undefined; - - if (!stateObj) { - return nothing; - } - - const name = this._config.name || stateObj.attributes.friendly_name || ""; - const icon = this._config.icon; - const iconColor = this._config.icon_color; - - const picture = this._config.use_entity_picture ? getEntityPicture(stateObj) : undefined; - - const stateDisplay = this.hass.formatEntityState - ? this.hass.formatEntityState(stateObj) - : computeStateDisplay( - this.hass.localize, - stateObj, - this.hass.locale, - this.hass.config, - this.hass.entities - ); - - const active = isActive(stateObj); - - const content = computeInfoDisplay( - this._config.content_info ?? "state", - name, - stateDisplay, - stateObj, - this.hass + const name = this._config.name || stateObj.attributes.friendly_name || ""; + const icon = this._config.icon; + const iconColor = this._config.icon_color; + + const picture = this._config.use_entity_picture + ? getEntityPicture(stateObj) + : undefined; + + const stateDisplay = this.hass.formatEntityState + ? this.hass.formatEntityState(stateObj) + : computeStateDisplay( + this.hass.localize, + stateObj, + this.hass.locale, + this.hass.config, + this.hass.entities ); - const rtl = computeRTL(this.hass); - - return html` - - ${!picture ? this.renderIcon(stateObj, icon, iconColor, active) : nothing} - ${content ? html`${content}` : nothing} - - `; - } - - renderIcon( - stateObj: HassEntity, - icon: string | undefined, - iconColor: string | undefined, - active: boolean - ): TemplateResult { - const iconStyle = {}; - if (iconColor) { - const iconRgbColor = computeRgbColor(iconColor); - iconStyle["--color"] = `rgb(${iconRgbColor})`; - } - return html` - - `; - } - - static get styles(): CSSResultGroup { - return css` - mushroom-chip { - cursor: pointer; - } - ha-state-icon.active { - color: var(--color); - } - `; + const active = isActive(stateObj); + + const content = computeInfoDisplay( + this._config.content_info ?? "state", + name, + stateDisplay, + stateObj, + this.hass + ); + + const rtl = computeRTL(this.hass); + + return html` + + ${!picture + ? this.renderIcon(stateObj, icon, iconColor, active) + : nothing} + ${content ? html`${content}` : nothing} + + `; + } + + renderIcon( + stateObj: HassEntity, + icon: string | undefined, + iconColor: string | undefined, + active: boolean + ): TemplateResult { + const iconStyle = {}; + if (iconColor) { + const iconRgbColor = computeRgbColor(iconColor); + iconStyle["--color"] = `rgb(${iconRgbColor})`; } + return html` + + `; + } + + static get styles(): CSSResultGroup { + return css` + mushroom-chip { + cursor: pointer; + } + ha-state-icon.active { + color: var(--color); + } + `; + } } diff --git a/src/cards/chips-card/chips/light-chip-editor.ts b/src/cards/chips-card/chips/light-chip-editor.ts index 3578898c8..7c0666702 100644 --- a/src/cards/chips-card/chips/light-chip-editor.ts +++ b/src/cards/chips-card/chips/light-chip-editor.ts @@ -12,65 +12,71 @@ import { LIGHT_ENTITY_DOMAINS } from "../../light-card/const"; import { LIGHT_LABELS } from "../../light-card/light-card-editor"; const SCHEMA: HaFormSchema[] = [ - { name: "entity", selector: { entity: { domain: LIGHT_ENTITY_DOMAINS } } }, - { - type: "grid", - name: "", - schema: [ - { name: "name", selector: { text: {} } }, - { name: "content_info", selector: { mush_info: {} } }, - ], - }, - { - type: "grid", - name: "", - schema: [ - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - { name: "use_light_color", selector: { boolean: {} } }, - ], - }, - ...computeActionsFormSchema(), + { name: "entity", selector: { entity: { domain: LIGHT_ENTITY_DOMAINS } } }, + { + type: "grid", + name: "", + schema: [ + { name: "name", selector: { text: {} } }, + { name: "content_info", selector: { mush_info: {} } }, + ], + }, + { + type: "grid", + name: "", + schema: [ + { + name: "icon", + selector: { icon: {} }, + context: { icon_entity: "entity" }, + }, + { name: "use_light_color", selector: { boolean: {} } }, + ], + }, + ...computeActionsFormSchema(), ]; @customElement(computeChipEditorComponentName("light")) export class LightChipEditor extends LitElement implements LovelaceChipEditor { - @property({ attribute: false }) public hass?: HomeAssistant; + @property({ attribute: false }) public hass?: HomeAssistant; - @state() private _config?: LightChipConfig; + @state() private _config?: LightChipConfig; - public setConfig(config: LightChipConfig): void { - this._config = config; - } - - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); - - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - if (LIGHT_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.light.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + public setConfig(config: LightChipConfig): void { + this._config = config; + } - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - return html` - - `; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); } + if (LIGHT_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.light.${schema.name}`); + } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/chips-card/chips/light-chip.ts b/src/cards/chips-card/chips/light-chip.ts index b4994e547..500a501e1 100644 --- a/src/cards/chips-card/chips/light-chip.ts +++ b/src/cards/chips-card/chips/light-chip.ts @@ -3,142 +3,147 @@ import { customElement, property, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - computeRTL, - computeStateDisplay, - handleAction, - hasAction, - HomeAssistant, - isActive, - LightEntity, + actionHandler, + ActionHandlerEvent, + computeRTL, + computeStateDisplay, + handleAction, + hasAction, + HomeAssistant, + isActive, + LightEntity, } from "../../../ha"; import { computeInfoDisplay } from "../../../utils/info"; import { - computeChipComponentName, - computeChipEditorComponentName, + computeChipComponentName, + computeChipEditorComponentName, } from "../../../utils/lovelace/chip/chip-element"; -import { LightChipConfig, LovelaceChip } from "../../../utils/lovelace/chip/types"; +import { + LightChipConfig, + LovelaceChip, +} from "../../../utils/lovelace/chip/types"; import { LovelaceChipEditor } from "../../../utils/lovelace/types"; import { getRGBColor, isColorSuperLight } from "../../light-card/utils"; @customElement(computeChipComponentName("light")) export class LightChip extends LitElement implements LovelaceChip { - public static async getConfigElement(): Promise { - await import("./light-chip-editor"); - return document.createElement( - computeChipEditorComponentName("light") - ) as LovelaceChipEditor; - } - - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - const lights = entities.filter((e) => e.split(".")[0] === "light"); - return { - type: `light`, - entity: lights[0], - }; + public static async getConfigElement(): Promise { + await import("./light-chip-editor"); + return document.createElement( + computeChipEditorComponentName("light") + ) as LovelaceChipEditor; + } + + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + const lights = entities.filter((e) => e.split(".")[0] === "light"); + return { + type: `light`, + entity: lights[0], + }; + } + + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: LightChipConfig; + + public setConfig(config: LightChipConfig): void { + this._config = { + tap_action: { + action: "toggle", + }, + hold_action: { + action: "more-info", + }, + ...config, + }; + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + protected render() { + if (!this.hass || !this._config || !this._config.entity) { + return nothing; } - @property({ attribute: false }) public hass?: HomeAssistant; - - @state() private _config?: LightChipConfig; + const entityId = this._config.entity; + const stateObj = this.hass.states[entityId] as LightEntity | undefined; - public setConfig(config: LightChipConfig): void { - this._config = { - tap_action: { - action: "toggle", - }, - hold_action: { - action: "more-info", - }, - ...config, - }; + if (!stateObj) { + return nothing; } - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); - } - - protected render() { - if (!this.hass || !this._config || !this._config.entity) { - return nothing; - } - - const entityId = this._config.entity; - const stateObj = this.hass.states[entityId] as LightEntity | undefined; - - if (!stateObj) { - return nothing; - } - - const name = this._config.name || stateObj.attributes.friendly_name || ""; - const icon = this._config.icon; - - const stateDisplay = this.hass.formatEntityState - ? this.hass.formatEntityState(stateObj) - : computeStateDisplay( - this.hass.localize, - stateObj, - this.hass.locale, - this.hass.config, - this.hass.entities - ); - - const active = isActive(stateObj); - - const lightRgbColor = getRGBColor(stateObj); - const iconStyle = {}; - if (lightRgbColor && this._config?.use_light_color) { - const color = lightRgbColor.join(","); - iconStyle["--color"] = `rgb(${color})`; - if (isColorSuperLight(lightRgbColor)) { - iconStyle["--color"] = `rgba(var(--rgb-primary-text-color), 0.2)`; - } - } - - const content = computeInfoDisplay( - this._config.content_info ?? "state", - name, - stateDisplay, - stateObj, - this.hass + const name = this._config.name || stateObj.attributes.friendly_name || ""; + const icon = this._config.icon; + + const stateDisplay = this.hass.formatEntityState + ? this.hass.formatEntityState(stateObj) + : computeStateDisplay( + this.hass.localize, + stateObj, + this.hass.locale, + this.hass.config, + this.hass.entities ); - const rtl = computeRTL(this.hass); - - return html` - - - ${content ? html`${content}` : nothing} - - `; - } + const active = isActive(stateObj); - static get styles(): CSSResultGroup { - return css` - :host { - --color: rgb(var(--rgb-state-light)); - } - mushroom-chip { - cursor: pointer; - } - ha-state-icon.active { - color: var(--color); - } - `; + const lightRgbColor = getRGBColor(stateObj); + const iconStyle = {}; + if (lightRgbColor && this._config?.use_light_color) { + const color = lightRgbColor.join(","); + iconStyle["--color"] = `rgb(${color})`; + if (isColorSuperLight(lightRgbColor)) { + iconStyle["--color"] = `rgba(var(--rgb-primary-text-color), 0.2)`; + } } + + const content = computeInfoDisplay( + this._config.content_info ?? "state", + name, + stateDisplay, + stateObj, + this.hass + ); + + const rtl = computeRTL(this.hass); + + return html` + + + ${content ? html`${content}` : nothing} + + `; + } + + static get styles(): CSSResultGroup { + return css` + :host { + --color: rgb(var(--rgb-state-light)); + } + mushroom-chip { + cursor: pointer; + } + ha-state-icon.active { + color: var(--color); + } + `; + } } diff --git a/src/cards/chips-card/chips/menu-chip-editor.ts b/src/cards/chips-card/chips/menu-chip-editor.ts index e6db8588b..d3c29b864 100644 --- a/src/cards/chips-card/chips/menu-chip-editor.ts +++ b/src/cards/chips-card/chips/menu-chip-editor.ts @@ -8,40 +8,42 @@ import { LovelaceChipEditor } from "../../../utils/lovelace/types"; import { DEFAULT_MENU_ICON } from "./menu-chip"; const SCHEMA: HaFormSchema[] = [ - { name: "icon", selector: { icon: { placeholder: DEFAULT_MENU_ICON } } }, + { name: "icon", selector: { icon: { placeholder: DEFAULT_MENU_ICON } } }, ]; @customElement(computeChipEditorComponentName("menu")) export class MenuChipEditor extends LitElement implements LovelaceChipEditor { - @property({ attribute: false }) public hass?: HomeAssistant; + @property({ attribute: false }) public hass?: HomeAssistant; - @state() private _config?: EntityChipConfig; + @state() private _config?: EntityChipConfig; - public setConfig(config: EntityChipConfig): void { - this._config = config; - } + public setConfig(config: EntityChipConfig): void { + this._config = config; + } - private _computeLabel = (schema: HaFormSchema) => { - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; - - protected render() { - if (!this.hass || !this._config) { - return nothing; - } - - return html` - - `; - } + private _computeLabel = (schema: HaFormSchema) => { + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/chips-card/chips/menu-chip.ts b/src/cards/chips-card/chips/menu-chip.ts index 0f9788e3d..e754bdaa6 100644 --- a/src/cards/chips-card/chips/menu-chip.ts +++ b/src/cards/chips-card/chips/menu-chip.ts @@ -1,65 +1,84 @@ -import { css, CSSResultGroup, html, LitElement, nothing, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + nothing, + TemplateResult, +} from "lit"; import { customElement, property, state } from "lit/decorators.js"; -import { actionHandler, computeRTL, fireEvent, HomeAssistant } from "../../../ha"; import { - computeChipComponentName, - computeChipEditorComponentName, + actionHandler, + computeRTL, + fireEvent, + HomeAssistant, +} from "../../../ha"; +import { + computeChipComponentName, + computeChipEditorComponentName, } from "../../../utils/lovelace/chip/chip-element"; -import { LovelaceChip, MenuChipConfig } from "../../../utils/lovelace/chip/types"; +import { + LovelaceChip, + MenuChipConfig, +} from "../../../utils/lovelace/chip/types"; import { LovelaceChipEditor } from "../../../utils/lovelace/types"; export const DEFAULT_MENU_ICON = "mdi:menu"; @customElement(computeChipComponentName("menu")) export class MenuChip extends LitElement implements LovelaceChip { - public static async getConfigElement(): Promise { - await import("./menu-chip-editor"); - return document.createElement(computeChipEditorComponentName("menu")) as LovelaceChipEditor; - } + public static async getConfigElement(): Promise { + await import("./menu-chip-editor"); + return document.createElement( + computeChipEditorComponentName("menu") + ) as LovelaceChipEditor; + } - public static async getStubConfig(_hass: HomeAssistant): Promise { - return { - type: `menu`, - }; - } + public static async getStubConfig( + _hass: HomeAssistant + ): Promise { + return { + type: `menu`, + }; + } - @property({ attribute: false }) public hass?: HomeAssistant; + @property({ attribute: false }) public hass?: HomeAssistant; - @state() private _config?: MenuChipConfig; + @state() private _config?: MenuChipConfig; - public setConfig(config: MenuChipConfig): void { - this._config = config; - } + public setConfig(config: MenuChipConfig): void { + this._config = config; + } + + private _handleAction() { + fireEvent(this, "hass-toggle-menu" as any); + } - private _handleAction() { - fireEvent(this, "hass-toggle-menu" as any); + protected render() { + if (!this.hass || !this._config) { + return nothing; } - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + const icon = this._config.icon || DEFAULT_MENU_ICON; - const icon = this._config.icon || DEFAULT_MENU_ICON; + const rtl = computeRTL(this.hass); - const rtl = computeRTL(this.hass); + return html` + + + + `; + } - return html` - - - - `; - } - - static get styles(): CSSResultGroup { - return css` - mushroom-chip { - cursor: pointer; - } - `; - } + static get styles(): CSSResultGroup { + return css` + mushroom-chip { + cursor: pointer; + } + `; + } } diff --git a/src/cards/chips-card/chips/spacer-chip.ts b/src/cards/chips-card/chips/spacer-chip.ts index f7310bd61..5bfea8eee 100644 --- a/src/cards/chips-card/chips/spacer-chip.ts +++ b/src/cards/chips-card/chips/spacer-chip.ts @@ -5,13 +5,13 @@ import { computeChipComponentName } from "../../../utils/lovelace/chip/chip-elem @customElement(computeChipComponentName("spacer")) export class SpacerChip extends LitElement implements LovelaceChip { - public setConfig(): void {} + public setConfig(): void {} - static get styles(): CSSResultGroup { - return css` - :host { - flex-grow: 1; - } - `; - } + static get styles(): CSSResultGroup { + return css` + :host { + flex-grow: 1; + } + `; + } } diff --git a/src/cards/chips-card/chips/template-chip-editor.ts b/src/cards/chips-card/chips/template-chip-editor.ts index e6ed58033..3d46d63b5 100644 --- a/src/cards/chips-card/chips/template-chip-editor.ts +++ b/src/cards/chips-card/chips/template-chip-editor.ts @@ -11,70 +11,72 @@ import { LovelaceChipEditor } from "../../../utils/lovelace/types"; import { TEMPLATE_LABELS } from "../../template-card/template-card-editor"; const SCHEMA: HaFormSchema[] = [ - { name: "entity", selector: { entity: {} } }, - { - name: "icon", - selector: { template: {} }, - }, - { - name: "icon_color", - selector: { template: {} }, - }, - { - name: "picture", - selector: { template: {} }, - }, - { - name: "content", - selector: { template: {} }, - }, - ...computeActionsFormSchema(), + { name: "entity", selector: { entity: {} } }, + { + name: "icon", + selector: { template: {} }, + }, + { + name: "icon_color", + selector: { template: {} }, + }, + { + name: "picture", + selector: { template: {} }, + }, + { + name: "content", + selector: { template: {} }, + }, + ...computeActionsFormSchema(), ]; @customElement(computeChipEditorComponentName("template")) export class EntityChipEditor extends LitElement implements LovelaceChipEditor { - @property({ attribute: false }) public hass?: HomeAssistant; + @property({ attribute: false }) public hass?: HomeAssistant; - @state() private _config?: TemplateChipConfig; + @state() private _config?: TemplateChipConfig; - public setConfig(config: TemplateChipConfig): void { - this._config = config; - } - - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + public setConfig(config: TemplateChipConfig): void { + this._config = config; + } - if (schema.name === "entity") { - return `${this.hass!.localize( - "ui.panel.lovelace.editor.card.generic.entity" - )} (${customLocalize("editor.card.template.entity_extra")})`; - } - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - if (TEMPLATE_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.template.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - protected render() { - if (!this.hass || !this._config) { - return nothing; - } - - return html` - - `; + if (schema.name === "entity") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.entity" + )} (${customLocalize("editor.card.template.entity_extra")})`; + } + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); } + if (TEMPLATE_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.template.${schema.name}`); + } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/chips-card/chips/template-chip.ts b/src/cards/chips-card/chips/template-chip.ts index 7c63f6abb..b0e8aa35a 100644 --- a/src/cards/chips-card/chips/template-chip.ts +++ b/src/cards/chips-card/chips/template-chip.ts @@ -1,32 +1,35 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { - css, - CSSResultGroup, - html, - LitElement, - nothing, - PropertyValues, - TemplateResult, + css, + CSSResultGroup, + html, + LitElement, + nothing, + PropertyValues, + TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - computeRTL, - handleAction, - hasAction, - HomeAssistant, - RenderTemplateResult, - subscribeRenderTemplate, + actionHandler, + ActionHandlerEvent, + computeRTL, + handleAction, + hasAction, + HomeAssistant, + RenderTemplateResult, + subscribeRenderTemplate, } from "../../../ha"; import { computeRgbColor } from "../../../utils/colors"; import { getWeatherSvgIcon } from "../../../utils/icons/weather-icon"; import { - computeChipComponentName, - computeChipEditorComponentName, + computeChipComponentName, + computeChipEditorComponentName, } from "../../../utils/lovelace/chip/chip-element"; -import { LovelaceChip, TemplateChipConfig } from "../../../utils/lovelace/chip/types"; +import { + LovelaceChip, + TemplateChipConfig, +} from "../../../utils/lovelace/chip/types"; import { LovelaceChipEditor } from "../../../utils/lovelace/types"; import { weatherSVGStyles } from "../../../utils/weather"; @@ -35,221 +38,229 @@ type TemplateKey = (typeof TEMPLATE_KEYS)[number]; @customElement(computeChipComponentName("template")) export class TemplateChip extends LitElement implements LovelaceChip { - public static async getConfigElement(): Promise { - await import("./template-chip-editor"); - return document.createElement( - computeChipEditorComponentName("template") - ) as LovelaceChipEditor; - } + public static async getConfigElement(): Promise { + await import("./template-chip-editor"); + return document.createElement( + computeChipEditorComponentName("template") + ) as LovelaceChipEditor; + } - public static async getStubConfig(_hass: HomeAssistant): Promise { - return { - type: `template`, - }; - } + public static async getStubConfig( + _hass: HomeAssistant + ): Promise { + return { + type: `template`, + }; + } - @property({ attribute: false }) public hass?: HomeAssistant; - - @state() private _config?: TemplateChipConfig; - - @state() private _templateResults: Partial< - Record - > = {}; - - @state() private _unsubRenderTemplates: Map> = new Map(); - - public setConfig(config: TemplateChipConfig): void { - TEMPLATE_KEYS.forEach((key) => { - if (this._config?.[key] !== config[key] || this._config?.entity != config.entity) { - this._tryDisconnectKey(key); - } - }); - this._config = { - tap_action: { - action: "toggle", - }, - hold_action: { - action: "more-info", - }, - ...config, - }; - } + @property({ attribute: false }) public hass?: HomeAssistant; - public connectedCallback() { - super.connectedCallback(); - this._tryConnect(); - } + @state() private _config?: TemplateChipConfig; - public disconnectedCallback() { - this._tryDisconnect(); - } + @state() private _templateResults: Partial< + Record + > = {}; - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); - } + @state() private _unsubRenderTemplates: Map< + TemplateKey, + Promise + > = new Map(); - public isTemplate(key: TemplateKey) { - const value = this._config?.[key]; - return value?.includes("{"); - } + public setConfig(config: TemplateChipConfig): void { + TEMPLATE_KEYS.forEach((key) => { + if ( + this._config?.[key] !== config[key] || + this._config?.entity != config.entity + ) { + this._tryDisconnectKey(key); + } + }); + this._config = { + tap_action: { + action: "toggle", + }, + hold_action: { + action: "more-info", + }, + ...config, + }; + } - private getValue(key: TemplateKey) { - return this.isTemplate(key) - ? this._templateResults[key]?.result?.toString() - : this._config?.[key]; - } + public connectedCallback() { + super.connectedCallback(); + this._tryConnect(); + } - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + public disconnectedCallback() { + this._tryDisconnect(); + } - const icon = this.getValue("icon"); - const iconColor = this.getValue("icon_color"); - const content = this.getValue("content"); - const picture = this.getValue("picture"); - - const rtl = computeRTL(this.hass); - const weatherSvg = getWeatherSvgIcon(icon); - - return html` - - ${!picture - ? weatherSvg - ? weatherSvg - : icon - ? this.renderIcon(icon, iconColor) - : nothing - : nothing} - ${content ? this.renderContent(content) : nothing} - - `; - } + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } - protected renderIcon(icon: string, iconColor?: string): TemplateResult { - const iconStyle = {}; - if (iconColor) { - const iconRgbColor = computeRgbColor(iconColor); - iconStyle["--color"] = `rgb(${iconRgbColor})`; - } - return html``; - } + public isTemplate(key: TemplateKey) { + const value = this._config?.[key]; + return value?.includes("{"); + } - protected renderContent(content: string): TemplateResult { - return html`${content}`; + private getValue(key: TemplateKey) { + return this.isTemplate(key) + ? this._templateResults[key]?.result?.toString() + : this._config?.[key]; + } + + protected render() { + if (!this.hass || !this._config) { + return nothing; } - protected updated(changedProps: PropertyValues): void { - super.updated(changedProps); - if (!this._config || !this.hass) { - return; - } + const icon = this.getValue("icon"); + const iconColor = this.getValue("icon_color"); + const content = this.getValue("content"); + const picture = this.getValue("picture"); - this._tryConnect(); - } + const rtl = computeRTL(this.hass); + const weatherSvg = getWeatherSvgIcon(icon); - private async _tryConnect(): Promise { - TEMPLATE_KEYS.forEach((key) => { - this._tryConnectKey(key); - }); + return html` + + ${!picture + ? weatherSvg + ? weatherSvg + : icon + ? this.renderIcon(icon, iconColor) + : nothing + : nothing} + ${content ? this.renderContent(content) : nothing} + + `; + } + + protected renderIcon(icon: string, iconColor?: string): TemplateResult { + const iconStyle = {}; + if (iconColor) { + const iconRgbColor = computeRgbColor(iconColor); + iconStyle["--color"] = `rgb(${iconRgbColor})`; } + return html``; + } - private async _tryConnectKey(key: TemplateKey): Promise { - if ( - this._unsubRenderTemplates.get(key) !== undefined || - !this.hass || - !this._config || - !this.isTemplate(key) - ) { - return; - } + protected renderContent(content: string): TemplateResult { + return html`${content}`; + } - try { - const sub = subscribeRenderTemplate( - this.hass.connection, - (result) => { - this._templateResults = { - ...this._templateResults, - [key]: result, - }; - }, - { - template: this._config[key] ?? "", - entity_ids: this._config.entity_id, - variables: { - config: this._config, - user: this.hass.user!.name, - entity: this._config.entity, - }, - strict: true, - } - ); - this._unsubRenderTemplates.set(key, sub); - await sub; - } catch (_err) { - const result = { - result: this._config[key] ?? "", - listeners: { - all: false, - domains: [], - entities: [], - time: false, - }, - }; - this._templateResults = { - ...this._templateResults, - [key]: result, - }; - this._unsubRenderTemplates.delete(key); - } + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if (!this._config || !this.hass) { + return; } - private async _tryDisconnect(): Promise { - TEMPLATE_KEYS.forEach((key) => { - this._tryDisconnectKey(key); - }); + + this._tryConnect(); + } + + private async _tryConnect(): Promise { + TEMPLATE_KEYS.forEach((key) => { + this._tryConnectKey(key); + }); + } + + private async _tryConnectKey(key: TemplateKey): Promise { + if ( + this._unsubRenderTemplates.get(key) !== undefined || + !this.hass || + !this._config || + !this.isTemplate(key) + ) { + return; } - private async _tryDisconnectKey(key: TemplateKey): Promise { - const unsubRenderTemplate = this._unsubRenderTemplates.get(key); - if (!unsubRenderTemplate) { - return; + try { + const sub = subscribeRenderTemplate( + this.hass.connection, + (result) => { + this._templateResults = { + ...this._templateResults, + [key]: result, + }; + }, + { + template: this._config[key] ?? "", + entity_ids: this._config.entity_id, + variables: { + config: this._config, + user: this.hass.user!.name, + entity: this._config.entity, + }, + strict: true, } + ); + this._unsubRenderTemplates.set(key, sub); + await sub; + } catch (_err) { + const result = { + result: this._config[key] ?? "", + listeners: { + all: false, + domains: [], + entities: [], + time: false, + }, + }; + this._templateResults = { + ...this._templateResults, + [key]: result, + }; + this._unsubRenderTemplates.delete(key); + } + } + private async _tryDisconnect(): Promise { + TEMPLATE_KEYS.forEach((key) => { + this._tryDisconnectKey(key); + }); + } - try { - const unsub = await unsubRenderTemplate; - unsub(); - this._unsubRenderTemplates.delete(key); - } catch (err: any) { - if (err.code === "not_found" || err.code === "template_error") { - // If we get here, the connection was probably already closed. Ignore. - } else { - throw err; - } - } + private async _tryDisconnectKey(key: TemplateKey): Promise { + const unsubRenderTemplate = this._unsubRenderTemplates.get(key); + if (!unsubRenderTemplate) { + return; } - static get styles(): CSSResultGroup { - return css` - mushroom-chip { - cursor: pointer; - } - ha-state-icon { - color: var(--color); - } - ${weatherSVGStyles} - `; + try { + const unsub = await unsubRenderTemplate; + unsub(); + this._unsubRenderTemplates.delete(key); + } catch (err: any) { + if (err.code === "not_found" || err.code === "template_error") { + // If we get here, the connection was probably already closed. Ignore. + } else { + throw err; + } } + } + + static get styles(): CSSResultGroup { + return css` + mushroom-chip { + cursor: pointer; + } + ha-state-icon { + color: var(--color); + } + ${weatherSVGStyles} + `; + } } diff --git a/src/cards/chips-card/chips/weather-chip-editor.ts b/src/cards/chips-card/chips/weather-chip-editor.ts index e081d829f..f8f1a7c67 100644 --- a/src/cards/chips-card/chips/weather-chip-editor.ts +++ b/src/cards/chips-card/chips/weather-chip-editor.ts @@ -13,60 +13,72 @@ import { LovelaceChipEditor } from "../../../utils/lovelace/types"; const WEATHER_ENTITY_DOMAINS = ["weather"]; const WEATHER_LABELS = ["show_conditions", "show_temperature"]; -const actions: UiAction[] = ["more-info", "navigate", "url", "call-service", "assist", "none"]; +const actions: UiAction[] = [ + "more-info", + "navigate", + "url", + "call-service", + "assist", + "none", +]; const SCHEMA: HaFormSchema[] = [ - { name: "entity", selector: { entity: { domain: WEATHER_ENTITY_DOMAINS } } }, - { - type: "grid", - name: "", - schema: [ - { name: "show_conditions", selector: { boolean: {} } }, - { name: "show_temperature", selector: { boolean: {} } }, - ], - }, - ...computeActionsFormSchema(actions), + { name: "entity", selector: { entity: { domain: WEATHER_ENTITY_DOMAINS } } }, + { + type: "grid", + name: "", + schema: [ + { name: "show_conditions", selector: { boolean: {} } }, + { name: "show_temperature", selector: { boolean: {} } }, + ], + }, + ...computeActionsFormSchema(actions), ]; @customElement(computeChipEditorComponentName("weather")) -export class WeatherChipEditor extends LitElement implements LovelaceChipEditor { - @property({ attribute: false }) public hass?: HomeAssistant; - - @state() private _config?: WeatherChipConfig; - - public setConfig(config: WeatherChipConfig): void { - this._config = config; - } +export class WeatherChipEditor + extends LitElement + implements LovelaceChipEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + @state() private _config?: WeatherChipConfig; - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - if (WEATHER_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.weather.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + public setConfig(config: WeatherChipConfig): void { + this._config = config; + } - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - return html` - - `; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); } + if (WEATHER_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.weather.${schema.name}`); + } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/chips-card/chips/weather-chip.ts b/src/cards/chips-card/chips/weather-chip.ts index 709d71cd6..de540c01c 100644 --- a/src/cards/chips-card/chips/weather-chip.ts +++ b/src/cards/chips-card/chips/weather-chip.ts @@ -1,118 +1,123 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { - actionHandler, - ActionHandlerEvent, - computeRTL, - computeStateDisplay, - formatNumber, - handleAction, - hasAction, - HomeAssistant, + actionHandler, + ActionHandlerEvent, + computeRTL, + computeStateDisplay, + formatNumber, + handleAction, + hasAction, + HomeAssistant, } from "../../../ha"; import { - computeChipComponentName, - computeChipEditorComponentName, + computeChipComponentName, + computeChipEditorComponentName, } from "../../../utils/lovelace/chip/chip-element"; -import { LovelaceChip, WeatherChipConfig } from "../../../utils/lovelace/chip/types"; +import { + LovelaceChip, + WeatherChipConfig, +} from "../../../utils/lovelace/chip/types"; import { LovelaceChipEditor } from "../../../utils/lovelace/types"; import { getWeatherStateSVG, weatherSVGStyles } from "../../../utils/weather"; import { HassEntity } from "home-assistant-js-websocket"; @customElement(computeChipComponentName("weather")) export class WeatherChip extends LitElement implements LovelaceChip { - public static async getConfigElement(): Promise { - await import("./weather-chip-editor"); - return document.createElement( - computeChipEditorComponentName("weather") - ) as LovelaceChipEditor; - } - - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - const weathers = entities.filter((e) => e.split(".")[0] === "weather"); - return { - type: `weather`, - entity: weathers[0], - }; + public static async getConfigElement(): Promise { + await import("./weather-chip-editor"); + return document.createElement( + computeChipEditorComponentName("weather") + ) as LovelaceChipEditor; + } + + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + const weathers = entities.filter((e) => e.split(".")[0] === "weather"); + return { + type: `weather`, + entity: weathers[0], + }; + } + + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: WeatherChipConfig; + + public setConfig(config: WeatherChipConfig): void { + this._config = config; + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + protected render() { + if (!this.hass || !this._config || !this._config.entity) { + return nothing; } - @property({ attribute: false }) public hass?: HomeAssistant; - - @state() private _config?: WeatherChipConfig; + const entityId = this._config.entity; + const stateObj = this.hass.states[entityId] as HassEntity | undefined; - public setConfig(config: WeatherChipConfig): void { - this._config = config; + if (!stateObj) { + return nothing; } - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); + const weatherIcon = getWeatherStateSVG(stateObj.state, true); + + const displayLabels: string[] = []; + + if (this._config.show_conditions) { + const stateDisplay = this.hass.formatEntityState + ? this.hass.formatEntityState(stateObj) + : computeStateDisplay( + this.hass.localize, + stateObj, + this.hass.locale, + this.hass.config, + this.hass.entities + ); + displayLabels.push(stateDisplay); } - protected render() { - if (!this.hass || !this._config || !this._config.entity) { - return nothing; - } - - const entityId = this._config.entity; - const stateObj = this.hass.states[entityId] as HassEntity | undefined; - - if (!stateObj) { - return nothing; - } - - const weatherIcon = getWeatherStateSVG(stateObj.state, true); - - const displayLabels: string[] = []; - - if (this._config.show_conditions) { - const stateDisplay = this.hass.formatEntityState - ? this.hass.formatEntityState(stateObj) - : computeStateDisplay( - this.hass.localize, - stateObj, - this.hass.locale, - this.hass.config, - this.hass.entities - ); - displayLabels.push(stateDisplay); - } - - if (this._config.show_temperature) { - const temperatureDisplay = `${formatNumber( - stateObj.attributes.temperature, - this.hass.locale - )} ${this.hass.config.unit_system.temperature}`; - displayLabels.push(temperatureDisplay); - } - - const rtl = computeRTL(this.hass); - - return html` - - ${weatherIcon} - ${displayLabels.length > 0 - ? html`${displayLabels.join(" / ")}` - : nothing} - - `; + if (this._config.show_temperature) { + const temperatureDisplay = `${formatNumber( + stateObj.attributes.temperature, + this.hass.locale + )} ${this.hass.config.unit_system.temperature}`; + displayLabels.push(temperatureDisplay); } - static get styles(): CSSResultGroup { - return [ - weatherSVGStyles, - css` - mushroom-chip { - cursor: pointer; - } - `, - ]; - } + const rtl = computeRTL(this.hass); + + return html` + + ${weatherIcon} + ${displayLabels.length > 0 + ? html`${displayLabels.join(" / ")}` + : nothing} + + `; + } + + static get styles(): CSSResultGroup { + return [ + weatherSVGStyles, + css` + mushroom-chip { + cursor: pointer; + } + `, + ]; + } } diff --git a/src/cards/climate-card/climate-card-config.ts b/src/cards/climate-card/climate-card-config.ts index 7f4a87a21..9f5a92df1 100644 --- a/src/cards/climate-card/climate-card-config.ts +++ b/src/cards/climate-card/climate-card-config.ts @@ -1,38 +1,48 @@ import { array, assign, boolean, object, optional, string } from "superstruct"; import { HvacMode, LovelaceCardConfig } from "../../ha"; -import { ActionsSharedConfig, actionsSharedConfigStruct } from "../../shared/config/actions-config"; import { - AppearanceSharedConfig, - appearanceSharedConfigStruct, + ActionsSharedConfig, + actionsSharedConfigStruct, +} from "../../shared/config/actions-config"; +import { + AppearanceSharedConfig, + appearanceSharedConfigStruct, } from "../../shared/config/appearance-config"; -import { EntitySharedConfig, entitySharedConfigStruct } from "../../shared/config/entity-config"; +import { + EntitySharedConfig, + entitySharedConfigStruct, +} from "../../shared/config/entity-config"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; export const HVAC_MODES: HvacMode[] = [ - "auto", - "heat_cool", - "heat", - "cool", - "dry", - "fan_only", - "off", + "auto", + "heat_cool", + "heat", + "cool", + "dry", + "fan_only", + "off", ]; export type ClimateCardConfig = LovelaceCardConfig & - EntitySharedConfig & - AppearanceSharedConfig & - ActionsSharedConfig & { - show_temperature_control?: false; - hvac_modes?: HvacMode[]; - collapsible_controls?: boolean; - }; + EntitySharedConfig & + AppearanceSharedConfig & + ActionsSharedConfig & { + show_temperature_control?: false; + hvac_modes?: HvacMode[]; + collapsible_controls?: boolean; + }; export const climateCardConfigStruct = assign( - lovelaceCardConfigStruct, - assign(entitySharedConfigStruct, appearanceSharedConfigStruct, actionsSharedConfigStruct), - object({ - show_temperature_control: optional(boolean()), - hvac_modes: optional(array(string())), - collapsible_controls: optional(boolean()), - }) + lovelaceCardConfigStruct, + assign( + entitySharedConfigStruct, + appearanceSharedConfigStruct, + actionsSharedConfigStruct + ), + object({ + show_temperature_control: optional(boolean()), + hvac_modes: optional(array(string())), + collapsible_controls: optional(boolean()), + }) ); diff --git a/src/cards/climate-card/climate-card-editor.ts b/src/cards/climate-card/climate-card-editor.ts index e58fc5c39..8d5a5121f 100644 --- a/src/cards/climate-card/climate-card-editor.ts +++ b/src/cards/climate-card/climate-card-editor.ts @@ -10,85 +10,96 @@ import { MushroomBaseElement } from "../../utils/base-element"; import { GENERIC_LABELS } from "../../utils/form/generic-fields"; import { HaFormSchema } from "../../utils/form/ha-form"; import { loadHaComponents } from "../../utils/loader"; -import { ClimateCardConfig, climateCardConfigStruct, HVAC_MODES } from "./climate-card-config"; +import { + ClimateCardConfig, + climateCardConfigStruct, + HVAC_MODES, +} from "./climate-card-config"; import { CLIMATE_CARD_EDITOR_NAME, CLIMATE_ENTITY_DOMAINS } from "./const"; const CLIMATE_LABELS = ["hvac_modes", "show_temperature_control"] as string[]; const computeSchema = memoizeOne((localize: LocalizeFunc): HaFormSchema[] => [ - { name: "entity", selector: { entity: { domain: CLIMATE_ENTITY_DOMAINS } } }, - { name: "name", selector: { text: {} } }, - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - ...APPEARANCE_FORM_SCHEMA, - { - type: "grid", - name: "", - schema: [ - { - name: "hvac_modes", - selector: { - select: { - options: HVAC_MODES.map((mode) => ({ - value: mode, - label: localize(`component.climate.entity_component._.state.${mode}`), - })), - mode: "dropdown", - multiple: true, - }, - }, - }, - { name: "show_temperature_control", selector: { boolean: {} } }, - { name: "collapsible_controls", selector: { boolean: {} } }, - ], - }, - ...computeActionsFormSchema(), + { name: "entity", selector: { entity: { domain: CLIMATE_ENTITY_DOMAINS } } }, + { name: "name", selector: { text: {} } }, + { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, + ...APPEARANCE_FORM_SCHEMA, + { + type: "grid", + name: "", + schema: [ + { + name: "hvac_modes", + selector: { + select: { + options: HVAC_MODES.map((mode) => ({ + value: mode, + label: localize( + `component.climate.entity_component._.state.${mode}` + ), + })), + mode: "dropdown", + multiple: true, + }, + }, + }, + { name: "show_temperature_control", selector: { boolean: {} } }, + { name: "collapsible_controls", selector: { boolean: {} } }, + ], + }, + ...computeActionsFormSchema(), ]); @customElement(CLIMATE_CARD_EDITOR_NAME) -export class ClimateCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: ClimateCardConfig; +export class ClimateCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: ClimateCardConfig; - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } - public setConfig(config: ClimateCardConfig): void { - assert(config, climateCardConfigStruct); - this._config = config; - } + public setConfig(config: ClimateCardConfig): void { + assert(config, climateCardConfigStruct); + this._config = config; + } - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - if (CLIMATE_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.climate.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); + } + if (CLIMATE_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.climate.${schema.name}`); + } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + protected render() { + if (!this.hass || !this._config) { + return nothing; + } - const schema = computeSchema(this.hass!.localize); + const schema = computeSchema(this.hass!.localize); - return html` - - `; - } + return html` + + `; + } - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); - } + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/climate-card/climate-card.ts b/src/cards/climate-card/climate-card.ts index 734aba26a..7bdf551e0 100644 --- a/src/cards/climate-card/climate-card.ts +++ b/src/cards/climate-card/climate-card.ts @@ -1,22 +1,29 @@ -import { css, CSSResultGroup, html, nothing, PropertyValues, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + nothing, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - ClimateEntity, - computeRTL, - computeStateDisplay, - formatNumber, - handleAction, - hasAction, - HomeAssistant, - HvacMode, - isActive, - isAvailable, - LovelaceCard, - LovelaceCardEditor, + actionHandler, + ActionHandlerEvent, + ClimateEntity, + computeRTL, + computeStateDisplay, + formatNumber, + handleAction, + hasAction, + HomeAssistant, + HvacMode, + isActive, + isAvailable, + LovelaceCard, + LovelaceCardEditor, } from "../../ha"; import "../../shared/badge-icon"; import "../../shared/card"; @@ -30,269 +37,299 @@ import { cardStyle } from "../../utils/card-styles"; import { registerCustomCard } from "../../utils/custom-cards"; import { computeEntityPicture } from "../../utils/info"; import { ClimateCardConfig } from "./climate-card-config"; -import { CLIMATE_CARD_EDITOR_NAME, CLIMATE_CARD_NAME, CLIMATE_ENTITY_DOMAINS } from "./const"; +import { + CLIMATE_CARD_EDITOR_NAME, + CLIMATE_CARD_NAME, + CLIMATE_ENTITY_DOMAINS, +} from "./const"; import "./controls/climate-hvac-modes-control"; import { isHvacModesVisible } from "./controls/climate-hvac-modes-control"; import "./controls/climate-temperature-control"; import { isTemperatureControlVisible } from "./controls/climate-temperature-control"; -import { getHvacActionColor, getHvacActionIcon, getHvacModeColor } from "./utils"; +import { + getHvacActionColor, + getHvacActionIcon, + getHvacModeColor, +} from "./utils"; type ClimateCardControl = "temperature_control" | "hvac_mode_control"; const CONTROLS_ICONS: Record = { - temperature_control: "mdi:thermometer", - hvac_mode_control: "mdi:thermostat", + temperature_control: "mdi:thermometer", + hvac_mode_control: "mdi:thermostat", }; registerCustomCard({ - type: CLIMATE_CARD_NAME, - name: "Mushroom Climate Card", - description: "Card for climate entity", + type: CLIMATE_CARD_NAME, + name: "Mushroom Climate Card", + description: "Card for climate entity", }); @customElement(CLIMATE_CARD_NAME) export class ClimateCard - extends MushroomBaseCard - implements LovelaceCard + extends MushroomBaseCard + implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./climate-card-editor"); - return document.createElement(CLIMATE_CARD_EDITOR_NAME) as LovelaceCardEditor; + public static async getConfigElement(): Promise { + await import("./climate-card-editor"); + return document.createElement( + CLIMATE_CARD_EDITOR_NAME + ) as LovelaceCardEditor; + } + + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + const climates = entities.filter((e) => + CLIMATE_ENTITY_DOMAINS.includes(e.split(".")[0]) + ); + return { + type: `custom:${CLIMATE_CARD_NAME}`, + entity: climates[0], + }; + } + + @state() private _activeControl?: ClimateCardControl; + + private get _controls(): ClimateCardControl[] { + if (!this._config || !this._stateObj) return []; + + const stateObj = this._stateObj; + const controls: ClimateCardControl[] = []; + if ( + isTemperatureControlVisible(stateObj) && + this._config.show_temperature_control + ) { + controls.push("temperature_control"); } - - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - const climates = entities.filter((e) => CLIMATE_ENTITY_DOMAINS.includes(e.split(".")[0])); - return { - type: `custom:${CLIMATE_CARD_NAME}`, - entity: climates[0], - }; + if (isHvacModesVisible(stateObj, this._config.hvac_modes)) { + controls.push("hvac_mode_control"); } - - @state() private _activeControl?: ClimateCardControl; - - private get _controls(): ClimateCardControl[] { - if (!this._config || !this._stateObj) return []; - - const stateObj = this._stateObj; - const controls: ClimateCardControl[] = []; - if (isTemperatureControlVisible(stateObj) && this._config.show_temperature_control) { - controls.push("temperature_control"); - } - if (isHvacModesVisible(stateObj, this._config.hvac_modes)) { - controls.push("hvac_mode_control"); - } - return controls; + return controls; + } + + protected get hasControls(): boolean { + return this._controls.length > 0; + } + + _onControlTap(ctrl, e): void { + e.stopPropagation(); + this._activeControl = ctrl; + } + + setConfig(config: ClimateCardConfig): void { + super.setConfig({ + tap_action: { + action: "toggle", + }, + hold_action: { + action: "more-info", + }, + ...config, + }); + this.updateActiveControl(); + } + + protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if (this.hass && changedProperties.has("hass")) { + this.updateActiveControl(); } - - protected get hasControls(): boolean { - return this._controls.length > 0; + } + + updateActiveControl() { + const isActiveControlSupported = this._activeControl + ? this._controls.includes(this._activeControl) + : false; + this._activeControl = isActiveControlSupported + ? this._activeControl + : this._controls[0]; + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + protected render() { + if (!this.hass || !this._config || !this._config.entity) { + return nothing; } - _onControlTap(ctrl, e): void { - e.stopPropagation(); - this._activeControl = ctrl; - } + const stateObj = this._stateObj; - setConfig(config: ClimateCardConfig): void { - super.setConfig({ - tap_action: { - action: "toggle", - }, - hold_action: { - action: "more-info", - }, - ...config, - }); - this.updateActiveControl(); + if (!stateObj) { + return this.renderNotFound(this._config); } - protected updated(changedProperties: PropertyValues) { - super.updated(changedProperties); - if (this.hass && changedProperties.has("hass")) { - this.updateActiveControl(); - } + const name = this._config.name || stateObj.attributes.friendly_name || ""; + const icon = this._config.icon; + const appearance = computeAppearance(this._config); + const picture = computeEntityPicture(stateObj, appearance.icon_type); + + let stateDisplay = this.hass.formatEntityState + ? this.hass.formatEntityState(stateObj) + : computeStateDisplay( + this.hass.localize, + stateObj, + this.hass.locale, + this.hass.config, + this.hass.entities + ); + if (stateObj.attributes.current_temperature !== null) { + const temperature = formatNumber( + stateObj.attributes.current_temperature, + this.hass.locale + ); + const unit = this.hass.config.unit_system.temperature; + stateDisplay += ` - ${temperature} ${unit}`; } - - updateActiveControl() { - const isActiveControlSupported = this._activeControl - ? this._controls.includes(this._activeControl) - : false; - this._activeControl = isActiveControlSupported ? this._activeControl : this._controls[0]; - } - - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); + const rtl = computeRTL(this.hass); + + const isControlVisible = + (!this._config.collapsible_controls || isActive(stateObj)) && + this._controls.length; + + return html` + + + + ${picture + ? this.renderPicture(picture) + : this.renderIcon(stateObj, icon)} + ${this.renderBadge(stateObj)} + ${this.renderStateInfo(stateObj, appearance, name, stateDisplay)}; + + ${isControlVisible + ? html` +
+ ${this.renderActiveControl(stateObj)} + ${this.renderOtherControls()} +
+ ` + : nothing} +
+
+ `; + } + + protected renderIcon(stateObj: ClimateEntity, icon?: string): TemplateResult { + const available = isAvailable(stateObj); + const color = getHvacModeColor(stateObj.state as HvacMode); + const iconStyle = {}; + iconStyle["--icon-color"] = `rgb(${color})`; + iconStyle["--shape-color"] = `rgba(${color}, 0.2)`; + + return html` + + + + `; + } + + protected renderBadge(entity: ClimateEntity) { + const unavailable = !isAvailable(entity); + if (unavailable) { + return super.renderBadge(entity); + } else { + return this.renderActionBadge(entity); } - - protected render() { - if (!this.hass || !this._config || !this._config.entity) { - return nothing; - } - - const stateObj = this._stateObj; - - if (!stateObj) { - return this.renderNotFound(this._config); - } - - const name = this._config.name || stateObj.attributes.friendly_name || ""; - const icon = this._config.icon; - const appearance = computeAppearance(this._config); - const picture = computeEntityPicture(stateObj, appearance.icon_type); - - let stateDisplay = this.hass.formatEntityState - ? this.hass.formatEntityState(stateObj) - : computeStateDisplay( - this.hass.localize, - stateObj, - this.hass.locale, - this.hass.config, - this.hass.entities - ); - if (stateObj.attributes.current_temperature !== null) { - const temperature = formatNumber( - stateObj.attributes.current_temperature, - this.hass.locale - ); - const unit = this.hass.config.unit_system.temperature; - stateDisplay += ` - ${temperature} ${unit}`; - } - const rtl = computeRTL(this.hass); - - const isControlVisible = - (!this._config.collapsible_controls || isActive(stateObj)) && this._controls.length; - + } + + renderActionBadge(entity: ClimateEntity) { + const hvac_action = entity.attributes.hvac_action; + if (!hvac_action || hvac_action == "off") return nothing; + + const color = getHvacActionColor(hvac_action); + const icon = getHvacActionIcon(hvac_action); + + if (!icon) return nothing; + + return html` + + `; + } + + private renderOtherControls(): TemplateResult | null { + const otherControls = this._controls.filter( + (control) => control != this._activeControl + ); + + return html` + ${otherControls.map( + (ctrl) => html` + this._onControlTap(ctrl, e)}> + + + ` + )} + `; + } + + private renderActiveControl(entity: ClimateEntity) { + const hvac_modes = this._config!.hvac_modes ?? []; + const appearance = computeAppearance(this._config!); + + switch (this._activeControl) { + case "temperature_control": return html` - - - - ${picture ? this.renderPicture(picture) : this.renderIcon(stateObj, icon)} - ${this.renderBadge(stateObj)} - ${this.renderStateInfo(stateObj, appearance, name, stateDisplay)}; - - ${isControlVisible - ? html` -
- ${this.renderActiveControl(stateObj)} - ${this.renderOtherControls()} -
- ` - : nothing} -
-
+ `; - } - - protected renderIcon(stateObj: ClimateEntity, icon?: string): TemplateResult { - const available = isAvailable(stateObj); - const color = getHvacModeColor(stateObj.state as HvacMode); - const iconStyle = {}; - iconStyle["--icon-color"] = `rgb(${color})`; - iconStyle["--shape-color"] = `rgba(${color}, 0.2)`; - + case "hvac_mode_control": return html` - - - + `; + default: + return nothing; } - - protected renderBadge(entity: ClimateEntity) { - const unavailable = !isAvailable(entity); - if (unavailable) { - return super.renderBadge(entity); - } else { - return this.renderActionBadge(entity); + } + + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + mushroom-state-item { + cursor: pointer; } - } - - renderActionBadge(entity: ClimateEntity) { - const hvac_action = entity.attributes.hvac_action; - if (!hvac_action || hvac_action == "off") return nothing; - - const color = getHvacActionColor(hvac_action); - const icon = getHvacActionIcon(hvac_action); - - if (!icon) return nothing; - - return html` - - `; - } - - private renderOtherControls(): TemplateResult | null { - const otherControls = this._controls.filter((control) => control != this._activeControl); - - return html` - ${otherControls.map( - (ctrl) => html` - this._onControlTap(ctrl, e)}> - - - ` - )} - `; - } - - private renderActiveControl(entity: ClimateEntity) { - const hvac_modes = this._config!.hvac_modes ?? []; - const appearance = computeAppearance(this._config!); - - switch (this._activeControl) { - case "temperature_control": - return html` - - `; - case "hvac_mode_control": - return html` - - `; - default: - return nothing; + mushroom-climate-temperature-control, + mushroom-climate-hvac-modes-control { + flex: 1; } - } - - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - mushroom-state-item { - cursor: pointer; - } - mushroom-climate-temperature-control, - mushroom-climate-hvac-modes-control { - flex: 1; - } - `, - ]; - } + `, + ]; + } } diff --git a/src/cards/climate-card/controls/climate-hvac-modes-control.ts b/src/cards/climate-card/controls/climate-hvac-modes-control.ts index 3d065a81f..f12235e51 100644 --- a/src/cards/climate-card/controls/climate-hvac-modes-control.ts +++ b/src/cards/climate-card/controls/climate-hvac-modes-control.ts @@ -2,70 +2,72 @@ import { html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; import { styleMap } from "lit/directives/style-map.js"; import { - ClimateEntity, - compareClimateHvacModes, - computeRTL, - HomeAssistant, - HvacMode, - isAvailable, + ClimateEntity, + compareClimateHvacModes, + computeRTL, + HomeAssistant, + HvacMode, + isAvailable, } from "../../../ha"; import "../../../shared/button"; import "../../../shared/button-group"; import { getHvacModeColor, getHvacModeIcon } from "../utils"; export const isHvacModesVisible = (entity: ClimateEntity, modes?: HvacMode[]) => - (entity.attributes.hvac_modes || []).some((mode) => (modes ?? []).includes(mode)); + (entity.attributes.hvac_modes || []).some((mode) => + (modes ?? []).includes(mode) + ); @customElement("mushroom-climate-hvac-modes-control") export class ClimateHvacModesControl extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public entity!: ClimateEntity; + @property({ attribute: false }) public entity!: ClimateEntity; - @property({ attribute: false }) public modes!: HvacMode[]; + @property({ attribute: false }) public modes!: HvacMode[]; - @property() public fill: boolean = false; + @property() public fill: boolean = false; - private callService(e: CustomEvent) { - e.stopPropagation(); - const mode = (e.target! as any).mode as HvacMode; - this.hass.callService("climate", "set_hvac_mode", { - entity_id: this.entity!.entity_id, - hvac_mode: mode, - }); - } - - protected render(): TemplateResult { - const rtl = computeRTL(this.hass); + private callService(e: CustomEvent) { + e.stopPropagation(); + const mode = (e.target! as any).mode as HvacMode; + this.hass.callService("climate", "set_hvac_mode", { + entity_id: this.entity!.entity_id, + hvac_mode: mode, + }); + } - const modes = this.entity.attributes.hvac_modes - .filter((mode) => (this.modes ?? []).includes(mode)) - .sort(compareClimateHvacModes); + protected render(): TemplateResult { + const rtl = computeRTL(this.hass); - return html` - - ${modes.map((mode) => this.renderModeButton(mode))} - - `; - } + const modes = this.entity.attributes.hvac_modes + .filter((mode) => (this.modes ?? []).includes(mode)) + .sort(compareClimateHvacModes); - private renderModeButton(mode: HvacMode) { - const iconStyle = {}; - const color = mode === "off" ? "var(--rgb-grey)" : getHvacModeColor(mode); - if (mode === this.entity.state) { - iconStyle["--icon-color"] = `rgb(${color})`; - iconStyle["--bg-color"] = `rgba(${color}, 0.2)`; - } + return html` + + ${modes.map((mode) => this.renderModeButton(mode))} + + `; + } - return html` - - - - `; + private renderModeButton(mode: HvacMode) { + const iconStyle = {}; + const color = mode === "off" ? "var(--rgb-grey)" : getHvacModeColor(mode); + if (mode === this.entity.state) { + iconStyle["--icon-color"] = `rgb(${color})`; + iconStyle["--bg-color"] = `rgba(${color}, 0.2)`; } + + return html` + + + + `; + } } diff --git a/src/cards/climate-card/controls/climate-temperature-control.ts b/src/cards/climate-card/controls/climate-temperature-control.ts index 9bfda4139..96f709f75 100644 --- a/src/cards/climate-card/controls/climate-temperature-control.ts +++ b/src/cards/climate-card/controls/climate-temperature-control.ts @@ -1,121 +1,128 @@ import { html, LitElement, nothing, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; import { styleMap } from "lit/directives/style-map.js"; -import { ClimateEntity, computeRTL, HomeAssistant, isAvailable, UNIT_F } from "../../../ha"; +import { + ClimateEntity, + computeRTL, + HomeAssistant, + isAvailable, + UNIT_F, +} from "../../../ha"; import "../../../shared/button"; import "../../../shared/button-group"; import "../../../shared/input-number"; export const isTemperatureControlVisible = (entity: ClimateEntity) => - entity.attributes.temperature != null || - (entity.attributes.target_temp_low != null && entity.attributes.target_temp_high != null); + entity.attributes.temperature != null || + (entity.attributes.target_temp_low != null && + entity.attributes.target_temp_high != null); @customElement("mushroom-climate-temperature-control") export class ClimateTemperatureControl extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public entity!: ClimateEntity; + @property({ attribute: false }) public entity!: ClimateEntity; - @property() public fill: boolean = false; + @property() public fill: boolean = false; - private get _stepSize(): number { - if (this.entity.attributes.target_temp_step) { - return this.entity.attributes.target_temp_step; - } - return this.hass!.config.unit_system.temperature === UNIT_F ? 1 : 0.5; + private get _stepSize(): number { + if (this.entity.attributes.target_temp_step) { + return this.entity.attributes.target_temp_step; } + return this.hass!.config.unit_system.temperature === UNIT_F ? 1 : 0.5; + } - onValueChange(e: CustomEvent<{ value: number }>): void { - const value = e.detail.value; - this.hass!.callService("climate", "set_temperature", { - entity_id: this.entity.entity_id, - temperature: value, - }); - } + onValueChange(e: CustomEvent<{ value: number }>): void { + const value = e.detail.value; + this.hass!.callService("climate", "set_temperature", { + entity_id: this.entity.entity_id, + temperature: value, + }); + } - onLowValueChange(e: CustomEvent<{ value: number }>): void { - const value = e.detail.value; - this.hass!.callService("climate", "set_temperature", { - entity_id: this.entity.entity_id, - target_temp_low: value, - target_temp_high: this.entity.attributes.target_temp_high, - }); - } + onLowValueChange(e: CustomEvent<{ value: number }>): void { + const value = e.detail.value; + this.hass!.callService("climate", "set_temperature", { + entity_id: this.entity.entity_id, + target_temp_low: value, + target_temp_high: this.entity.attributes.target_temp_high, + }); + } - onHighValueChange(e: CustomEvent<{ value: number }>): void { - const value = e.detail.value; - this.hass!.callService("climate", "set_temperature", { - entity_id: this.entity.entity_id, - target_temp_low: this.entity.attributes.target_temp_low, - target_temp_high: value, - }); - } + onHighValueChange(e: CustomEvent<{ value: number }>): void { + const value = e.detail.value; + this.hass!.callService("climate", "set_temperature", { + entity_id: this.entity.entity_id, + target_temp_low: this.entity.attributes.target_temp_low, + target_temp_high: value, + }); + } - protected render(): TemplateResult { - const rtl = computeRTL(this.hass); + protected render(): TemplateResult { + const rtl = computeRTL(this.hass); - const available = isAvailable(this.entity); + const available = isAvailable(this.entity); - const formatOptions: Intl.NumberFormatOptions = - this._stepSize === 1 - ? { - maximumFractionDigits: 0, - } - : { - minimumFractionDigits: 1, - maximumFractionDigits: 1, - }; + const formatOptions: Intl.NumberFormatOptions = + this._stepSize === 1 + ? { + maximumFractionDigits: 0, + } + : { + minimumFractionDigits: 1, + maximumFractionDigits: 1, + }; - const modeStyle = (mode: "heat" | "cool") => ({ - "--bg-color": `rgba(var(--rgb-state-climate-${mode}), 0.05)`, - "--icon-color": `rgb(var(--rgb-state-climate-${mode}))`, - "--text-color": `rgb(var(--rgb-state-climate-${mode}))`, - }); + const modeStyle = (mode: "heat" | "cool") => ({ + "--bg-color": `rgba(var(--rgb-state-climate-${mode}), 0.05)`, + "--icon-color": `rgb(var(--rgb-state-climate-${mode}))`, + "--text-color": `rgb(var(--rgb-state-climate-${mode}))`, + }); - return html` - - ${this.entity.attributes.temperature != null - ? html` - - ` - : nothing} - ${this.entity.attributes.target_temp_low != null && - this.entity.attributes.target_temp_high != null - ? html` - - ` - : nothing} - - `; - } + return html` + + ${this.entity.attributes.temperature != null + ? html` + + ` + : nothing} + ${this.entity.attributes.target_temp_low != null && + this.entity.attributes.target_temp_high != null + ? html` + + ` + : nothing} + + `; + } } diff --git a/src/cards/climate-card/utils.ts b/src/cards/climate-card/utils.ts index d42223d19..1eddab581 100644 --- a/src/cards/climate-card/utils.ts +++ b/src/cards/climate-card/utils.ts @@ -1,53 +1,55 @@ import { HvacAction, HvacMode } from "../../ha"; export const CLIMATE_HVAC_MODE_COLORS: Record = { - auto: "var(--rgb-state-climate-auto)", - cool: "var(--rgb-state-climate-cool)", - dry: "var(--rgb-state-climate-dry)", - fan_only: "var(--rgb-state-climate-fan-only)", - heat: "var(--rgb-state-climate-heat)", - heat_cool: "var(--rgb-state-climate-heat-cool)", - off: "var(--rgb-state-climate-off)", + auto: "var(--rgb-state-climate-auto)", + cool: "var(--rgb-state-climate-cool)", + dry: "var(--rgb-state-climate-dry)", + fan_only: "var(--rgb-state-climate-fan-only)", + heat: "var(--rgb-state-climate-heat)", + heat_cool: "var(--rgb-state-climate-heat-cool)", + off: "var(--rgb-state-climate-off)", }; export const CLIMATE_HVAC_ACTION_COLORS: Record = { - cooling: "var(--rgb-state-climate-cool)", - drying: "var(--rgb-state-climate-dry)", - heating: "var(--rgb-state-climate-heat)", - idle: "var(--rgb-state-climate-idle)", - off: "var(--rgb-state-climate-off)", + cooling: "var(--rgb-state-climate-cool)", + drying: "var(--rgb-state-climate-dry)", + heating: "var(--rgb-state-climate-heat)", + idle: "var(--rgb-state-climate-idle)", + off: "var(--rgb-state-climate-off)", }; export const CLIMATE_HVAC_MODE_ICONS: Record = { - auto: "mdi:calendar-sync", - cool: "mdi:snowflake", - dry: "mdi:water-percent", - fan_only: "mdi:fan", - heat: "mdi:fire", - heat_cool: "mdi:autorenew", - off: "mdi:power", + auto: "mdi:calendar-sync", + cool: "mdi:snowflake", + dry: "mdi:water-percent", + fan_only: "mdi:fan", + heat: "mdi:fire", + heat_cool: "mdi:autorenew", + off: "mdi:power", }; export const CLIMATE_HVAC_ACTION_ICONS: Record = { - cooling: "mdi:snowflake", - drying: "mdi:water-percent", - heating: "mdi:fire", - idle: "mdi:clock-outline", - off: "mdi:power", + cooling: "mdi:snowflake", + drying: "mdi:water-percent", + heating: "mdi:fire", + idle: "mdi:clock-outline", + off: "mdi:power", }; export function getHvacModeColor(hvacMode: HvacMode): string { - return CLIMATE_HVAC_MODE_COLORS[hvacMode] ?? CLIMATE_HVAC_MODE_COLORS.off; + return CLIMATE_HVAC_MODE_COLORS[hvacMode] ?? CLIMATE_HVAC_MODE_COLORS.off; } export function getHvacActionColor(hvacAction: HvacAction): string { - return CLIMATE_HVAC_ACTION_COLORS[hvacAction] ?? CLIMATE_HVAC_ACTION_COLORS.off; + return ( + CLIMATE_HVAC_ACTION_COLORS[hvacAction] ?? CLIMATE_HVAC_ACTION_COLORS.off + ); } export function getHvacModeIcon(hvacMode: HvacMode): string { - return CLIMATE_HVAC_MODE_ICONS[hvacMode] ?? "mdi:thermostat"; + return CLIMATE_HVAC_MODE_ICONS[hvacMode] ?? "mdi:thermostat"; } export function getHvacActionIcon(hvacAction: HvacAction): string | undefined { - return CLIMATE_HVAC_ACTION_ICONS[hvacAction] ?? ""; + return CLIMATE_HVAC_ACTION_ICONS[hvacAction] ?? ""; } diff --git a/src/cards/cover-card/controls/cover-buttons-control.ts b/src/cards/cover-card/controls/cover-buttons-control.ts index 11faf7cc0..22ce45c22 100644 --- a/src/cards/cover-card/controls/cover-buttons-control.ts +++ b/src/cards/cover-card/controls/cover-buttons-control.ts @@ -1,98 +1,105 @@ import { html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; import { - computeRTL, - CoverEntity, - COVER_SUPPORT_CLOSE, - COVER_SUPPORT_OPEN, - COVER_SUPPORT_STOP, - HomeAssistant, - isAvailable, - isClosing, - isFullyClosed, - isFullyOpen, - isOpening, - supportsFeature, + computeRTL, + CoverEntity, + COVER_SUPPORT_CLOSE, + COVER_SUPPORT_OPEN, + COVER_SUPPORT_STOP, + HomeAssistant, + isAvailable, + isClosing, + isFullyClosed, + isFullyOpen, + isOpening, + supportsFeature, } from "../../../ha"; import "../../../shared/button"; import "../../../shared/button-group"; -import { computeCloseIcon, computeOpenIcon } from "../../../utils/icons/cover-icon"; +import { + computeCloseIcon, + computeOpenIcon, +} from "../../../utils/icons/cover-icon"; @customElement("mushroom-cover-buttons-control") export class CoverButtonsControl extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public entity!: CoverEntity; + @property({ attribute: false }) public entity!: CoverEntity; - @property() public fill: boolean = false; + @property() public fill: boolean = false; - private _onOpenTap(e: MouseEvent): void { - e.stopPropagation(); - this.hass.callService("cover", "open_cover", { - entity_id: this.entity.entity_id, - }); - } + private _onOpenTap(e: MouseEvent): void { + e.stopPropagation(); + this.hass.callService("cover", "open_cover", { + entity_id: this.entity.entity_id, + }); + } - private _onCloseTap(e: MouseEvent): void { - e.stopPropagation(); - this.hass.callService("cover", "close_cover", { - entity_id: this.entity.entity_id, - }); - } + private _onCloseTap(e: MouseEvent): void { + e.stopPropagation(); + this.hass.callService("cover", "close_cover", { + entity_id: this.entity.entity_id, + }); + } - private _onStopTap(e: MouseEvent): void { - e.stopPropagation(); - this.hass.callService("cover", "stop_cover", { - entity_id: this.entity.entity_id, - }); - } + private _onStopTap(e: MouseEvent): void { + e.stopPropagation(); + this.hass.callService("cover", "stop_cover", { + entity_id: this.entity.entity_id, + }); + } - private get openDisabled(): boolean { - const assumedState = this.entity.attributes.assumed_state === true; - return (isFullyOpen(this.entity) || isOpening(this.entity)) && !assumedState; - } + private get openDisabled(): boolean { + const assumedState = this.entity.attributes.assumed_state === true; + return ( + (isFullyOpen(this.entity) || isOpening(this.entity)) && !assumedState + ); + } - private get closedDisabled(): boolean { - const assumedState = this.entity.attributes.assumed_state === true; - return (isFullyClosed(this.entity) || isClosing(this.entity)) && !assumedState; - } + private get closedDisabled(): boolean { + const assumedState = this.entity.attributes.assumed_state === true; + return ( + (isFullyClosed(this.entity) || isClosing(this.entity)) && !assumedState + ); + } - protected render(): TemplateResult { - const rtl = computeRTL(this.hass); + protected render(): TemplateResult { + const rtl = computeRTL(this.hass); - return html` - - ${supportsFeature(this.entity, COVER_SUPPORT_OPEN) - ? html` - - - - ` - : undefined} - ${supportsFeature(this.entity, COVER_SUPPORT_STOP) - ? html` - - - - ` - : undefined} - ${supportsFeature(this.entity, COVER_SUPPORT_CLOSE) - ? html` - - - - ` - : undefined} - - `; - } + return html` + + ${supportsFeature(this.entity, COVER_SUPPORT_OPEN) + ? html` + + + + ` + : undefined} + ${supportsFeature(this.entity, COVER_SUPPORT_STOP) + ? html` + + + + ` + : undefined} + ${supportsFeature(this.entity, COVER_SUPPORT_CLOSE) + ? html` + + + + ` + : undefined} + + `; + } } diff --git a/src/cards/cover-card/controls/cover-position-control.ts b/src/cards/cover-card/controls/cover-position-control.ts index 0f1292088..718ed5993 100644 --- a/src/cards/cover-card/controls/cover-position-control.ts +++ b/src/cards/cover-card/controls/cover-position-control.ts @@ -6,50 +6,50 @@ import { getPosition } from "../utils"; @customElement("mushroom-cover-position-control") export class CoverPositionControl extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public entity!: CoverEntity; - - private onChange(e: CustomEvent<{ value: number }>): void { - const value = e.detail.value; - - this.hass.callService("cover", "set_cover_position", { - entity_id: this.entity.entity_id, - position: value, - }); - } - - onCurrentChange(e: CustomEvent<{ value?: number }>): void { - const value = e.detail.value; - this.dispatchEvent( - new CustomEvent("current-change", { - detail: { - value, - }, - }) - ); - } - - protected render(): TemplateResult { - const position = getPosition(this.entity); - - return html` - - `; - } - - static get styles(): CSSResultGroup { - return css` - mushroom-slider { - --main-color: var(--slider-color); - --bg-color: var(--slider-bg-color); - } - `; - } + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public entity!: CoverEntity; + + private onChange(e: CustomEvent<{ value: number }>): void { + const value = e.detail.value; + + this.hass.callService("cover", "set_cover_position", { + entity_id: this.entity.entity_id, + position: value, + }); + } + + onCurrentChange(e: CustomEvent<{ value?: number }>): void { + const value = e.detail.value; + this.dispatchEvent( + new CustomEvent("current-change", { + detail: { + value, + }, + }) + ); + } + + protected render(): TemplateResult { + const position = getPosition(this.entity); + + return html` + + `; + } + + static get styles(): CSSResultGroup { + return css` + mushroom-slider { + --main-color: var(--slider-color); + --bg-color: var(--slider-bg-color); + } + `; + } } diff --git a/src/cards/cover-card/controls/cover-tilt-position-control.ts b/src/cards/cover-card/controls/cover-tilt-position-control.ts index 9034439c6..6a3afd3c5 100644 --- a/src/cards/cover-card/controls/cover-tilt-position-control.ts +++ b/src/cards/cover-card/controls/cover-tilt-position-control.ts @@ -1,80 +1,92 @@ -import { css, CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + TemplateResult, + unsafeCSS, +} from "lit"; import { customElement, property } from "lit/decorators.js"; import { CoverEntity, HomeAssistant, isAvailable } from "../../../ha"; import "../../../shared/slider"; import { getTiltPosition } from "../utils"; -function createTiltSliderTrackBackgroundGradient(count: number = 24, minStrokeWidth: number = 0.2) { - const gradient: [number, string][] = []; +function createTiltSliderTrackBackgroundGradient( + count: number = 24, + minStrokeWidth: number = 0.2 +) { + const gradient: [number, string][] = []; - for (let i = 0; i < count; i++) { - const stopOffset1 = i / count; - const stopOffset2 = - stopOffset1 + (i / count ** 2) * (1 - minStrokeWidth) + minStrokeWidth / count; + for (let i = 0; i < count; i++) { + const stopOffset1 = i / count; + const stopOffset2 = + stopOffset1 + + (i / count ** 2) * (1 - minStrokeWidth) + + minStrokeWidth / count; - if (i !== 0) { - gradient.push([stopOffset1, "transparent"]); - } - gradient.push([stopOffset1, "var(--slider-bg-color)"]); - gradient.push([stopOffset2, "var(--slider-bg-color)"]); - gradient.push([stopOffset2, "transparent"]); + if (i !== 0) { + gradient.push([stopOffset1, "transparent"]); } + gradient.push([stopOffset1, "var(--slider-bg-color)"]); + gradient.push([stopOffset2, "var(--slider-bg-color)"]); + gradient.push([stopOffset2, "transparent"]); + } - return gradient; + return gradient; } const GRADIENT = createTiltSliderTrackBackgroundGradient(); @customElement("mushroom-cover-tilt-position-control") export class CoverTiltPositionControl extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public entity!: CoverEntity; + @property({ attribute: false }) public entity!: CoverEntity; - private onChange(e: CustomEvent<{ value: number }>): void { - const value = e.detail.value; + private onChange(e: CustomEvent<{ value: number }>): void { + const value = e.detail.value; - this.hass.callService("cover", "set_cover_tilt_position", { - entity_id: this.entity.entity_id, - tilt_position: value, - }); - } + this.hass.callService("cover", "set_cover_tilt_position", { + entity_id: this.entity.entity_id, + tilt_position: value, + }); + } - onCurrentChange(e: CustomEvent<{ value?: number }>): void { - const value = e.detail.value; - this.dispatchEvent( - new CustomEvent("current-change", { - detail: { - value, - }, - }) - ); - } + onCurrentChange(e: CustomEvent<{ value?: number }>): void { + const value = e.detail.value; + this.dispatchEvent( + new CustomEvent("current-change", { + detail: { + value, + }, + }) + ); + } - protected render(): TemplateResult { - const tilt = getTiltPosition(this.entity); + protected render(): TemplateResult { + const tilt = getTiltPosition(this.entity); - return html` - - `; - } + return html` + + `; + } - static get styles(): CSSResultGroup { - const gradient = GRADIENT.map( - ([stop, color]) => `${color} ${(stop as number) * 100}%` - ).join(", "); - return css` - mushroom-slider { - --main-color: var(--slider-color); - --bg-color: var(--slider-bg-color); - --gradient: -webkit-linear-gradient(right, ${unsafeCSS(gradient)}); - } - `; - } + static get styles(): CSSResultGroup { + const gradient = GRADIENT.map( + ([stop, color]) => `${color} ${(stop as number) * 100}%` + ).join(", "); + return css` + mushroom-slider { + --main-color: var(--slider-color); + --bg-color: var(--slider-bg-color); + --gradient: -webkit-linear-gradient(right, ${unsafeCSS(gradient)}); + } + `; + } } diff --git a/src/cards/cover-card/cover-card-config.ts b/src/cards/cover-card/cover-card-config.ts index cb68b59b6..dcfff3a30 100644 --- a/src/cards/cover-card/cover-card-config.ts +++ b/src/cards/cover-card/cover-card-config.ts @@ -1,28 +1,38 @@ import { assign, boolean, object, optional } from "superstruct"; -import { actionsSharedConfigStruct, ActionsSharedConfig } from "../../shared/config/actions-config"; import { - appearanceSharedConfigStruct, - AppearanceSharedConfig, + actionsSharedConfigStruct, + ActionsSharedConfig, +} from "../../shared/config/actions-config"; +import { + appearanceSharedConfigStruct, + AppearanceSharedConfig, } from "../../shared/config/appearance-config"; -import { entitySharedConfigStruct, EntitySharedConfig } from "../../shared/config/entity-config"; +import { + entitySharedConfigStruct, + EntitySharedConfig, +} from "../../shared/config/entity-config"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; import { LovelaceCardConfig } from "../../ha"; export type CoverCardConfig = LovelaceCardConfig & - EntitySharedConfig & - AppearanceSharedConfig & - ActionsSharedConfig & { - show_buttons_control?: false; - show_position_control?: false; - show_tilt_position_control?: false; - }; + EntitySharedConfig & + AppearanceSharedConfig & + ActionsSharedConfig & { + show_buttons_control?: false; + show_position_control?: false; + show_tilt_position_control?: false; + }; export const coverCardConfigStruct = assign( - lovelaceCardConfigStruct, - assign(entitySharedConfigStruct, appearanceSharedConfigStruct, actionsSharedConfigStruct), - object({ - show_buttons_control: optional(boolean()), - show_position_control: optional(boolean()), - show_tilt_position_control: optional(boolean()), - }) + lovelaceCardConfigStruct, + assign( + entitySharedConfigStruct, + appearanceSharedConfigStruct, + actionsSharedConfigStruct + ), + object({ + show_buttons_control: optional(boolean()), + show_position_control: optional(boolean()), + show_tilt_position_control: optional(boolean()), + }) ); diff --git a/src/cards/cover-card/cover-card-editor.ts b/src/cards/cover-card/cover-card-editor.ts index 1658da661..291022d0f 100644 --- a/src/cards/cover-card/cover-card-editor.ts +++ b/src/cards/cover-card/cover-card-editor.ts @@ -13,71 +13,76 @@ import { COVER_CARD_EDITOR_NAME, COVER_ENTITY_DOMAINS } from "./const"; import { CoverCardConfig, coverCardConfigStruct } from "./cover-card-config"; const COVER_LABELS = [ - "show_buttons_control", - "show_position_control", - "show_tilt_position_control", + "show_buttons_control", + "show_position_control", + "show_tilt_position_control", ]; const SCHEMA: HaFormSchema[] = [ - { name: "entity", selector: { entity: { domain: COVER_ENTITY_DOMAINS } } }, - { name: "name", selector: { text: {} } }, - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - ...APPEARANCE_FORM_SCHEMA, - { - type: "grid", - name: "", - schema: [ - { name: "show_position_control", selector: { boolean: {} } }, - { name: "show_tilt_position_control", selector: { boolean: {} } }, - { name: "show_buttons_control", selector: { boolean: {} } }, - ], - }, - ...computeActionsFormSchema(), + { name: "entity", selector: { entity: { domain: COVER_ENTITY_DOMAINS } } }, + { name: "name", selector: { text: {} } }, + { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, + ...APPEARANCE_FORM_SCHEMA, + { + type: "grid", + name: "", + schema: [ + { name: "show_position_control", selector: { boolean: {} } }, + { name: "show_tilt_position_control", selector: { boolean: {} } }, + { name: "show_buttons_control", selector: { boolean: {} } }, + ], + }, + ...computeActionsFormSchema(), ]; @customElement(COVER_CARD_EDITOR_NAME) -export class CoverCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: CoverCardConfig; +export class CoverCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: CoverCardConfig; - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } - - public setConfig(config: CoverCardConfig): void { - assert(config, coverCardConfigStruct); - this._config = config; - } - - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - if (COVER_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.cover.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + public setConfig(config: CoverCardConfig): void { + assert(config, coverCardConfigStruct); + this._config = config; + } - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - return html` - - `; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); } + if (COVER_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.cover.${schema.name}`); + } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/cover-card/cover-card.ts b/src/cards/cover-card/cover-card.ts index 9cd43597d..0efcccc92 100644 --- a/src/cards/cover-card/cover-card.ts +++ b/src/cards/cover-card/cover-card.ts @@ -1,20 +1,27 @@ -import { css, CSSResultGroup, html, nothing, PropertyValues, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + nothing, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - blankBeforePercent, - computeRTL, - computeStateDisplay, - CoverEntity, - handleAction, - hasAction, - HomeAssistant, - isAvailable, - LovelaceCard, - LovelaceCardEditor, + actionHandler, + ActionHandlerEvent, + blankBeforePercent, + computeRTL, + computeStateDisplay, + CoverEntity, + handleAction, + hasAction, + HomeAssistant, + isAvailable, + LovelaceCard, + LovelaceCardEditor, } from "../../ha"; import "../../shared/badge-icon"; import "../../shared/button"; @@ -29,287 +36,310 @@ import { cardStyle } from "../../utils/card-styles"; import { registerCustomCard } from "../../utils/custom-cards"; import { computeEntityPicture } from "../../utils/info"; import { Layout } from "../../utils/layout"; -import { COVER_CARD_EDITOR_NAME, COVER_CARD_NAME, COVER_ENTITY_DOMAINS } from "./const"; +import { + COVER_CARD_EDITOR_NAME, + COVER_CARD_NAME, + COVER_ENTITY_DOMAINS, +} from "./const"; import "./controls/cover-buttons-control"; import "./controls/cover-position-control"; import "./controls/cover-tilt-position-control"; import { CoverCardConfig } from "./cover-card-config"; import { getPosition, getStateColor } from "./utils"; -type CoverCardControl = "buttons_control" | "position_control" | "tilt_position_control"; +type CoverCardControl = + | "buttons_control" + | "position_control" + | "tilt_position_control"; const CONTROLS_ICONS: Record = { - buttons_control: "mdi:gesture-tap-button", - position_control: "mdi:gesture-swipe-horizontal", - tilt_position_control: "mdi:rotate-right", + buttons_control: "mdi:gesture-tap-button", + position_control: "mdi:gesture-swipe-horizontal", + tilt_position_control: "mdi:rotate-right", }; registerCustomCard({ - type: COVER_CARD_NAME, - name: "Mushroom Cover Card", - description: "Card for cover entity", + type: COVER_CARD_NAME, + name: "Mushroom Cover Card", + description: "Card for cover entity", }); @customElement(COVER_CARD_NAME) export class CoverCard - extends MushroomBaseCard - implements LovelaceCard + extends MushroomBaseCard + implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./cover-card-editor"); - return document.createElement(COVER_CARD_EDITOR_NAME) as LovelaceCardEditor; + public static async getConfigElement(): Promise { + await import("./cover-card-editor"); + return document.createElement(COVER_CARD_EDITOR_NAME) as LovelaceCardEditor; + } + + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + const covers = entities.filter((e) => + COVER_ENTITY_DOMAINS.includes(e.split(".")[0]) + ); + return { + type: `custom:${COVER_CARD_NAME}`, + entity: covers[0], + }; + } + + protected get hasControls(): boolean { + return this._controls.length > 0; + } + + @state() private _activeControl?: CoverCardControl; + + get _nextControl(): CoverCardControl | undefined { + if (this._activeControl) { + return ( + this._controls[this._controls.indexOf(this._activeControl) + 1] ?? + this._controls[0] + ); } - - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - const covers = entities.filter((e) => COVER_ENTITY_DOMAINS.includes(e.split(".")[0])); - return { - type: `custom:${COVER_CARD_NAME}`, - entity: covers[0], - }; - } - - protected get hasControls(): boolean { - return this._controls.length > 0; + return undefined; + } + + private _onNextControlTap(e): void { + e.stopPropagation(); + this._activeControl = this._nextControl; + } + + getCardSize(): number | Promise { + return 1; + } + + setConfig(config: CoverCardConfig): void { + super.setConfig({ + tap_action: { + action: "toggle", + }, + hold_action: { + action: "more-info", + }, + ...config, + }); + this.updateActiveControl(); + this.updatePosition(); + } + + private get _controls(): CoverCardControl[] { + if (!this._config || !this._stateObj) return []; + const controls: CoverCardControl[] = []; + if (this._config.show_buttons_control) { + controls.push("buttons_control"); } - - @state() private _activeControl?: CoverCardControl; - - get _nextControl(): CoverCardControl | undefined { - if (this._activeControl) { - return ( - this._controls[this._controls.indexOf(this._activeControl) + 1] ?? this._controls[0] - ); - } - return undefined; + if (this._config.show_position_control) { + controls.push("position_control"); } - - private _onNextControlTap(e): void { - e.stopPropagation(); - this._activeControl = this._nextControl; + if (this._config.show_tilt_position_control) { + controls.push("tilt_position_control"); } - - getCardSize(): number | Promise { - return 1; + return controls; + } + + updateActiveControl() { + const isActiveControlSupported = this._activeControl + ? this._controls.includes(this._activeControl) + : false; + this._activeControl = isActiveControlSupported + ? this._activeControl + : this._controls[0]; + } + + protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if (this.hass && changedProperties.has("hass")) { + this.updatePosition(); + this.updateActiveControl(); } + } - setConfig(config: CoverCardConfig): void { - super.setConfig({ - tap_action: { - action: "toggle", - }, - hold_action: { - action: "more-info", - }, - ...config, - }); - this.updateActiveControl(); - this.updatePosition(); - } + @state() + private position?: number; - private get _controls(): CoverCardControl[] { - if (!this._config || !this._stateObj) return []; - const controls: CoverCardControl[] = []; - if (this._config.show_buttons_control) { - controls.push("buttons_control"); - } - if (this._config.show_position_control) { - controls.push("position_control"); - } - if (this._config.show_tilt_position_control) { - controls.push("tilt_position_control"); - } - return controls; - } + updatePosition() { + this.position = undefined; + const stateObj = this._stateObj; - updateActiveControl() { - const isActiveControlSupported = this._activeControl - ? this._controls.includes(this._activeControl) - : false; - this._activeControl = isActiveControlSupported ? this._activeControl : this._controls[0]; - } + if (!stateObj) return; + this.position = getPosition(stateObj); + } - protected updated(changedProperties: PropertyValues) { - super.updated(changedProperties); - if (this.hass && changedProperties.has("hass")) { - this.updatePosition(); - this.updateActiveControl(); - } + private onCurrentPositionChange(e: CustomEvent<{ value?: number }>): void { + if (e.detail.value != null) { + this.position = e.detail.value; } + } - @state() - private position?: number; - - updatePosition() { - this.position = undefined; - const stateObj = this._stateObj; + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } - if (!stateObj) return; - this.position = getPosition(stateObj); + protected render() { + if (!this.hass || !this._config || !this._config.entity) { + return nothing; } - private onCurrentPositionChange(e: CustomEvent<{ value?: number }>): void { - if (e.detail.value != null) { - this.position = e.detail.value; - } - } + const stateObj = this._stateObj; - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); + if (!stateObj) { + return this.renderNotFound(this._config); } - protected render() { - if (!this.hass || !this._config || !this._config.entity) { - return nothing; - } - - const stateObj = this._stateObj; - - if (!stateObj) { - return this.renderNotFound(this._config); - } - - const name = this._config.name || stateObj.attributes.friendly_name || ""; - const icon = this._config.icon; - const appearance = computeAppearance(this._config); - const picture = computeEntityPicture(stateObj, appearance.icon_type); - - let stateDisplay = this.hass.formatEntityState - ? this.hass.formatEntityState(stateObj) - : computeStateDisplay( - this.hass.localize, - stateObj, - this.hass.locale, - this.hass.config, - this.hass.entities - ); - if (this.position) { - stateDisplay += ` - ${this.position}${blankBeforePercent(this.hass.locale)}%`; - } - - const rtl = computeRTL(this.hass); + const name = this._config.name || stateObj.attributes.friendly_name || ""; + const icon = this._config.icon; + const appearance = computeAppearance(this._config); + const picture = computeEntityPicture(stateObj, appearance.icon_type); + + let stateDisplay = this.hass.formatEntityState + ? this.hass.formatEntityState(stateObj) + : computeStateDisplay( + this.hass.localize, + stateObj, + this.hass.locale, + this.hass.config, + this.hass.entities + ); + if (this.position) { + stateDisplay += ` - ${this.position}${blankBeforePercent(this.hass.locale)}%`; + } + const rtl = computeRTL(this.hass); + + return html` + + + + ${picture + ? this.renderPicture(picture) + : this.renderIcon(stateObj, icon)} + ${this.renderBadge(stateObj)} + ${this.renderStateInfo(stateObj, appearance, name, stateDisplay)}; + + ${this._controls.length > 0 + ? html` +
+ ${this.renderActiveControl(stateObj, appearance.layout)} + ${this.renderNextControlButton()} +
+ ` + : nothing} +
+
+ `; + } + + protected renderIcon(stateObj: CoverEntity, icon?: string): TemplateResult { + const iconStyle = {}; + const available = isAvailable(stateObj); + const color = getStateColor(stateObj); + iconStyle["--icon-color"] = `rgb(${color})`; + iconStyle["--shape-color"] = `rgba(${color}, 0.2)`; + + return html` + + + `; + } + + private renderNextControlButton() { + if (!this._nextControl || this._nextControl == this._activeControl) + return nothing; + + return html` + + + + `; + } + + private renderActiveControl(stateObj: CoverEntity, layout?: Layout) { + switch (this._activeControl) { + case "buttons_control": return html` - - - - ${picture ? this.renderPicture(picture) : this.renderIcon(stateObj, icon)} - ${this.renderBadge(stateObj)} - ${this.renderStateInfo(stateObj, appearance, name, stateDisplay)}; - - ${this._controls.length > 0 - ? html` -
- ${this.renderActiveControl(stateObj, appearance.layout)} - ${this.renderNextControlButton()} -
- ` - : nothing} -
-
+ `; - } - - protected renderIcon(stateObj: CoverEntity, icon?: string): TemplateResult { - const iconStyle = {}; - const available = isAvailable(stateObj); - const color = getStateColor(stateObj); - iconStyle["--icon-color"] = `rgb(${color})`; - iconStyle["--shape-color"] = `rgba(${color}, 0.2)`; + case "position_control": { + const color = getStateColor(stateObj as CoverEntity); + const sliderStyle = {}; + sliderStyle["--slider-color"] = `rgb(${color})`; + sliderStyle["--slider-bg-color"] = `rgba(${color}, 0.2)`; return html` - - + `; - } - - private renderNextControlButton() { - if (!this._nextControl || this._nextControl == this._activeControl) return nothing; + } + case "tilt_position_control": { + const color = getStateColor(stateObj as CoverEntity); + const sliderStyle = {}; + sliderStyle["--slider-color"] = `rgb(${color})`; + sliderStyle["--slider-bg-color"] = `rgba(${color}, 0.2)`; return html` - - - + `; + } + default: + return nothing; } - - private renderActiveControl(stateObj: CoverEntity, layout?: Layout) { - switch (this._activeControl) { - case "buttons_control": - return html` - - `; - case "position_control": { - const color = getStateColor(stateObj as CoverEntity); - const sliderStyle = {}; - sliderStyle["--slider-color"] = `rgb(${color})`; - sliderStyle["--slider-bg-color"] = `rgba(${color}, 0.2)`; - - return html` - - `; - } - case "tilt_position_control": { - const color = getStateColor(stateObj as CoverEntity); - const sliderStyle = {}; - sliderStyle["--slider-color"] = `rgb(${color})`; - sliderStyle["--slider-bg-color"] = `rgba(${color}, 0.2)`; - - return html` - - `; - } - default: - return nothing; + } + + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + mushroom-state-item { + cursor: pointer; } - } - - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - mushroom-state-item { - cursor: pointer; - } - mushroom-shape-icon { - --icon-color: rgb(var(--rgb-state-cover)); - --shape-color: rgba(var(--rgb-state-cover), 0.2); - } - mushroom-cover-buttons-control, - mushroom-cover-position-control { - flex: 1; - } - mushroom-cover-tilt-position-control { - flex: 1; - } - `, - ]; - } + mushroom-shape-icon { + --icon-color: rgb(var(--rgb-state-cover)); + --shape-color: rgba(var(--rgb-state-cover), 0.2); + } + mushroom-cover-buttons-control, + mushroom-cover-position-control { + flex: 1; + } + mushroom-cover-tilt-position-control { + flex: 1; + } + `, + ]; + } } diff --git a/src/cards/cover-card/utils.ts b/src/cards/cover-card/utils.ts index 1ae091095..21325bf1a 100644 --- a/src/cards/cover-card/utils.ts +++ b/src/cards/cover-card/utils.ts @@ -1,24 +1,24 @@ import { CoverEntity } from "../../ha"; export function getPosition(entity: CoverEntity) { - return entity.attributes.current_position != null - ? Math.round(entity.attributes.current_position) - : undefined; + return entity.attributes.current_position != null + ? Math.round(entity.attributes.current_position) + : undefined; } export function getTiltPosition(entity: CoverEntity) { - return entity.attributes.current_tilt_position != null - ? Math.round(entity.attributes.current_tilt_position) - : undefined; + return entity.attributes.current_tilt_position != null + ? Math.round(entity.attributes.current_tilt_position) + : undefined; } export function getStateColor(entity: CoverEntity) { - const state = entity.state; - if (state === "open" || state === "opening") { - return "var(--rgb-state-cover-open)"; - } - if (state === "closed" || state === "closing") { - return "var(--rgb-state-cover-closed)"; - } - return "var(--rgb-disabled)"; + const state = entity.state; + if (state === "open" || state === "opening") { + return "var(--rgb-state-cover-open)"; + } + if (state === "closed" || state === "closing") { + return "var(--rgb-state-cover-closed)"; + } + return "var(--rgb-disabled)"; } diff --git a/src/cards/entity-card/entity-card-config.ts b/src/cards/entity-card/entity-card-config.ts index d43e260dd..ede0dc42d 100644 --- a/src/cards/entity-card/entity-card-config.ts +++ b/src/cards/entity-card/entity-card-config.ts @@ -1,24 +1,34 @@ import { assign, boolean, object, optional, string } from "superstruct"; -import { ActionsSharedConfig, actionsSharedConfigStruct } from "../../shared/config/actions-config"; import { - appearanceSharedConfigStruct, - AppearanceSharedConfig, + ActionsSharedConfig, + actionsSharedConfigStruct, +} from "../../shared/config/actions-config"; +import { + appearanceSharedConfigStruct, + AppearanceSharedConfig, } from "../../shared/config/appearance-config"; -import { entitySharedConfigStruct, EntitySharedConfig } from "../../shared/config/entity-config"; +import { + entitySharedConfigStruct, + EntitySharedConfig, +} from "../../shared/config/entity-config"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; import { LovelaceCardConfig } from "../../ha"; export type EntityCardConfig = LovelaceCardConfig & - EntitySharedConfig & - AppearanceSharedConfig & - ActionsSharedConfig & { - icon_color?: string; - }; + EntitySharedConfig & + AppearanceSharedConfig & + ActionsSharedConfig & { + icon_color?: string; + }; export const entityCardConfigStruct = assign( - lovelaceCardConfigStruct, - assign(entitySharedConfigStruct, appearanceSharedConfigStruct, actionsSharedConfigStruct), - object({ - icon_color: optional(string()), - }) + lovelaceCardConfigStruct, + assign( + entitySharedConfigStruct, + appearanceSharedConfigStruct, + actionsSharedConfigStruct + ), + object({ + icon_color: optional(string()), + }) ); diff --git a/src/cards/entity-card/entity-card-editor.ts b/src/cards/entity-card/entity-card-editor.ts index 1281254a5..18aed6bc5 100644 --- a/src/cards/entity-card/entity-card-editor.ts +++ b/src/cards/entity-card/entity-card-editor.ts @@ -13,60 +13,69 @@ import { ENTITY_CARD_EDITOR_NAME } from "./const"; import { EntityCardConfig, entityCardConfigStruct } from "./entity-card-config"; const SCHEMA: HaFormSchema[] = [ - { name: "entity", selector: { entity: {} } }, - { name: "name", selector: { text: {} } }, - { - type: "grid", - name: "", - schema: [ - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - { name: "icon_color", selector: { mush_color: {} } }, - ], - }, - ...APPEARANCE_FORM_SCHEMA, - ...computeActionsFormSchema(), + { name: "entity", selector: { entity: {} } }, + { name: "name", selector: { text: {} } }, + { + type: "grid", + name: "", + schema: [ + { + name: "icon", + selector: { icon: {} }, + context: { icon_entity: "entity" }, + }, + { name: "icon_color", selector: { mush_color: {} } }, + ], + }, + ...APPEARANCE_FORM_SCHEMA, + ...computeActionsFormSchema(), ]; @customElement(ENTITY_CARD_EDITOR_NAME) -export class EntityCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: EntityCardConfig; +export class EntityCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: EntityCardConfig; - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } - - public setConfig(config: EntityCardConfig): void { - assert(config, entityCardConfigStruct); - this._config = config; - } + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + public setConfig(config: EntityCardConfig): void { + assert(config, entityCardConfigStruct); + this._config = config; + } - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - protected render() { - if (!this.hass || !this._config) { - return nothing; - } - - return html` - - `; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/entity-card/entity-card.ts b/src/cards/entity-card/entity-card.ts index a70ad74bf..cc20e7fec 100644 --- a/src/cards/entity-card/entity-card.ts +++ b/src/cards/entity-card/entity-card.ts @@ -4,15 +4,15 @@ import { customElement } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - computeRTL, - handleAction, - hasAction, - HomeAssistant, - isActive, - LovelaceCard, - LovelaceCardEditor, + actionHandler, + ActionHandlerEvent, + computeRTL, + handleAction, + hasAction, + HomeAssistant, + isActive, + LovelaceCard, + LovelaceCardEditor, } from "../../ha"; import "../../shared/badge-icon"; import "../../shared/card"; @@ -30,103 +30,118 @@ import { ENTITY_CARD_EDITOR_NAME, ENTITY_CARD_NAME } from "./const"; import { EntityCardConfig } from "./entity-card-config"; registerCustomCard({ - type: ENTITY_CARD_NAME, - name: "Mushroom Entity Card", - description: "Card for all entities", + type: ENTITY_CARD_NAME, + name: "Mushroom Entity Card", + description: "Card for all entities", }); @customElement(ENTITY_CARD_NAME) -export class EntityCard extends MushroomBaseCard implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./entity-card-editor"); - return document.createElement(ENTITY_CARD_EDITOR_NAME) as LovelaceCardEditor; - } +export class EntityCard + extends MushroomBaseCard + implements LovelaceCard +{ + public static async getConfigElement(): Promise { + await import("./entity-card-editor"); + return document.createElement( + ENTITY_CARD_EDITOR_NAME + ) as LovelaceCardEditor; + } - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - return { - type: `custom:${ENTITY_CARD_NAME}`, - entity: entities[0], - }; - } + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + return { + type: `custom:${ENTITY_CARD_NAME}`, + entity: entities[0], + }; + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); + protected render() { + if (!this._config || !this.hass || !this._config.entity) { + return nothing; } - protected render() { - if (!this._config || !this.hass || !this._config.entity) { - return nothing; - } + const stateObj = this._stateObj; - const stateObj = this._stateObj; + if (!stateObj) { + return this.renderNotFound(this._config); + } - if (!stateObj) { - return this.renderNotFound(this._config); - } + const name = this._config.name || stateObj.attributes.friendly_name || ""; + const icon = this._config.icon; + const appearance = computeAppearance(this._config); - const name = this._config.name || stateObj.attributes.friendly_name || ""; - const icon = this._config.icon; - const appearance = computeAppearance(this._config); + const picture = computeEntityPicture(stateObj, appearance.icon_type); - const picture = computeEntityPicture(stateObj, appearance.icon_type); + const rtl = computeRTL(this.hass); - const rtl = computeRTL(this.hass); + return html` + + + + ${picture + ? this.renderPicture(picture) + : this.renderIcon(stateObj, icon)} + ${this.renderBadge(stateObj)} + ${this.renderStateInfo(stateObj, appearance, name)}; + + + + `; + } - return html` - - - - ${picture ? this.renderPicture(picture) : this.renderIcon(stateObj, icon)} - ${this.renderBadge(stateObj)} - ${this.renderStateInfo(stateObj, appearance, name)}; - - - - `; + renderIcon(stateObj: HassEntity, icon?: string): TemplateResult { + const active = isActive(stateObj); + const iconStyle = {}; + const iconColor = this._config?.icon_color; + if (iconColor) { + const iconRgbColor = computeRgbColor(iconColor); + iconStyle["--icon-color"] = `rgb(${iconRgbColor})`; + iconStyle["--shape-color"] = `rgba(${iconRgbColor}, 0.2)`; } + return html` + + + + `; + } - renderIcon(stateObj: HassEntity, icon?: string): TemplateResult { - const active = isActive(stateObj); - const iconStyle = {}; - const iconColor = this._config?.icon_color; - if (iconColor) { - const iconRgbColor = computeRgbColor(iconColor); - iconStyle["--icon-color"] = `rgb(${iconRgbColor})`; - iconStyle["--shape-color"] = `rgba(${iconRgbColor}, 0.2)`; + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + mushroom-state-item { + cursor: pointer; } - return html` - - - - `; - } - - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - mushroom-state-item { - cursor: pointer; - } - mushroom-shape-icon { - --icon-color: rgb(var(--rgb-state-entity)); - --shape-color: rgba(var(--rgb-state-entity), 0.2); - } - `, - ]; - } + mushroom-shape-icon { + --icon-color: rgb(var(--rgb-state-entity)); + --shape-color: rgba(var(--rgb-state-entity), 0.2); + } + `, + ]; + } } diff --git a/src/cards/fan-card/controls/fan-oscillate-control.ts b/src/cards/fan-card/controls/fan-oscillate-control.ts index 8ec149f6e..79dc5b083 100644 --- a/src/cards/fan-card/controls/fan-oscillate-control.ts +++ b/src/cards/fan-card/controls/fan-oscillate-control.ts @@ -8,46 +8,48 @@ import { isOscillating } from "../utils"; @customElement("mushroom-fan-oscillate-control") export class FanPercentageControl extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public entity!: HassEntity; + @property({ attribute: false }) public entity!: HassEntity; - private _onTap(e: MouseEvent): void { - e.stopPropagation(); - const oscillating = isOscillating(this.entity); + private _onTap(e: MouseEvent): void { + e.stopPropagation(); + const oscillating = isOscillating(this.entity); - this.hass.callService("fan", "oscillate", { - entity_id: this.entity.entity_id, - oscillating: !oscillating, - }); - } + this.hass.callService("fan", "oscillate", { + entity_id: this.entity.entity_id, + oscillating: !oscillating, + }); + } - protected render(): TemplateResult { - const oscillating = isOscillating(this.entity); - const active = isActive(this.entity); + protected render(): TemplateResult { + const oscillating = isOscillating(this.entity); + const active = isActive(this.entity); - return html` - - - - `; - } + return html` + + + + `; + } - static get styles(): CSSResultGroup { - return css` - :host { - display: flex; - } - mushroom-button.active { - --icon-color: rgb(var(--rgb-state-fan)); - --bg-color: rgba(var(--rgb-state-fan), 0.2); - } - `; - } + static get styles(): CSSResultGroup { + return css` + :host { + display: flex; + } + mushroom-button.active { + --icon-color: rgb(var(--rgb-state-fan)); + --bg-color: rgba(var(--rgb-state-fan), 0.2); + } + `; + } } diff --git a/src/cards/fan-card/controls/fan-percentage-control.ts b/src/cards/fan-card/controls/fan-percentage-control.ts index 1579af444..c775b60c6 100644 --- a/src/cards/fan-card/controls/fan-percentage-control.ts +++ b/src/cards/fan-card/controls/fan-percentage-control.ts @@ -7,51 +7,51 @@ import { computePercentageStep, getPercentage } from "../utils"; @customElement("mushroom-fan-percentage-control") export class FanPercentageControl extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public entity!: HassEntity; + @property({ attribute: false }) public entity!: HassEntity; - onChange(e: CustomEvent<{ value: number }>): void { - const value = e.detail.value; - this.hass.callService("fan", "set_percentage", { - entity_id: this.entity.entity_id, - percentage: value, - }); - } + onChange(e: CustomEvent<{ value: number }>): void { + const value = e.detail.value; + this.hass.callService("fan", "set_percentage", { + entity_id: this.entity.entity_id, + percentage: value, + }); + } - onCurrentChange(e: CustomEvent<{ value?: number }>): void { - const value = e.detail.value; - this.dispatchEvent( - new CustomEvent("current-change", { - detail: { - value, - }, - }) - ); - } + onCurrentChange(e: CustomEvent<{ value?: number }>): void { + const value = e.detail.value; + this.dispatchEvent( + new CustomEvent("current-change", { + detail: { + value, + }, + }) + ); + } - protected render(): TemplateResult { - const percentage = getPercentage(this.entity); + protected render(): TemplateResult { + const percentage = getPercentage(this.entity); - return html` - - `; - } + return html` + + `; + } - static get styles(): CSSResultGroup { - return css` - mushroom-slider { - --main-color: rgb(var(--rgb-state-fan)); - --bg-color: rgba(var(--rgb-state-fan), 0.2); - } - `; - } + static get styles(): CSSResultGroup { + return css` + mushroom-slider { + --main-color: rgb(var(--rgb-state-fan)); + --bg-color: rgba(var(--rgb-state-fan), 0.2); + } + `; + } } diff --git a/src/cards/fan-card/fan-card-config.ts b/src/cards/fan-card/fan-card-config.ts index 175c3c79e..3d0431ce4 100644 --- a/src/cards/fan-card/fan-card-config.ts +++ b/src/cards/fan-card/fan-card-config.ts @@ -1,30 +1,40 @@ import { assign, boolean, object, optional } from "superstruct"; -import { actionsSharedConfigStruct, ActionsSharedConfig } from "../../shared/config/actions-config"; import { - appearanceSharedConfigStruct, - AppearanceSharedConfig, + actionsSharedConfigStruct, + ActionsSharedConfig, +} from "../../shared/config/actions-config"; +import { + appearanceSharedConfigStruct, + AppearanceSharedConfig, } from "../../shared/config/appearance-config"; -import { entitySharedConfigStruct, EntitySharedConfig } from "../../shared/config/entity-config"; +import { + entitySharedConfigStruct, + EntitySharedConfig, +} from "../../shared/config/entity-config"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; import { LovelaceCardConfig } from "../../ha"; export type FanCardConfig = LovelaceCardConfig & - EntitySharedConfig & - AppearanceSharedConfig & - ActionsSharedConfig & { - icon_animation?: boolean; - show_percentage_control?: boolean; - show_oscillate_control?: boolean; - collapsible_controls?: boolean; - }; + EntitySharedConfig & + AppearanceSharedConfig & + ActionsSharedConfig & { + icon_animation?: boolean; + show_percentage_control?: boolean; + show_oscillate_control?: boolean; + collapsible_controls?: boolean; + }; export const fanCardConfigStruct = assign( - lovelaceCardConfigStruct, - assign(entitySharedConfigStruct, appearanceSharedConfigStruct, actionsSharedConfigStruct), - object({ - icon_animation: optional(boolean()), - show_percentage_control: optional(boolean()), - show_oscillate_control: optional(boolean()), - collapsible_controls: optional(boolean()), - }) + lovelaceCardConfigStruct, + assign( + entitySharedConfigStruct, + appearanceSharedConfigStruct, + actionsSharedConfigStruct + ), + object({ + icon_animation: optional(boolean()), + show_percentage_control: optional(boolean()), + show_oscillate_control: optional(boolean()), + collapsible_controls: optional(boolean()), + }) ); diff --git a/src/cards/fan-card/fan-card-editor.ts b/src/cards/fan-card/fan-card-editor.ts index faed797ca..7786da5ca 100644 --- a/src/cards/fan-card/fan-card-editor.ts +++ b/src/cards/fan-card/fan-card-editor.ts @@ -12,75 +12,88 @@ import { loadHaComponents } from "../../utils/loader"; import { FAN_CARD_EDITOR_NAME, FAN_ENTITY_DOMAINS } from "./const"; import { FanCardConfig, fanCardConfigStruct } from "./fan-card-config"; -const FAN_LABELS = ["icon_animation", "show_percentage_control", "show_oscillate_control"]; +const FAN_LABELS = [ + "icon_animation", + "show_percentage_control", + "show_oscillate_control", +]; const SCHEMA: HaFormSchema[] = [ - { name: "entity", selector: { entity: { domain: FAN_ENTITY_DOMAINS } } }, - { name: "name", selector: { text: {} } }, - { - type: "grid", - name: "", - schema: [ - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - { name: "icon_animation", selector: { boolean: {} } }, - ], - }, - ...APPEARANCE_FORM_SCHEMA, - { - type: "grid", - name: "", - schema: [ - { name: "show_percentage_control", selector: { boolean: {} } }, - { name: "show_oscillate_control", selector: { boolean: {} } }, - { name: "collapsible_controls", selector: { boolean: {} } }, - ], - }, - ...computeActionsFormSchema(), + { name: "entity", selector: { entity: { domain: FAN_ENTITY_DOMAINS } } }, + { name: "name", selector: { text: {} } }, + { + type: "grid", + name: "", + schema: [ + { + name: "icon", + selector: { icon: {} }, + context: { icon_entity: "entity" }, + }, + { name: "icon_animation", selector: { boolean: {} } }, + ], + }, + ...APPEARANCE_FORM_SCHEMA, + { + type: "grid", + name: "", + schema: [ + { name: "show_percentage_control", selector: { boolean: {} } }, + { name: "show_oscillate_control", selector: { boolean: {} } }, + { name: "collapsible_controls", selector: { boolean: {} } }, + ], + }, + ...computeActionsFormSchema(), ]; @customElement(FAN_CARD_EDITOR_NAME) -export class FanCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: FanCardConfig; - - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } - - public setConfig(config: FanCardConfig): void { - assert(config, fanCardConfigStruct); - this._config = config; - } +export class FanCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: FanCardConfig; - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - if (FAN_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.fan.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + public setConfig(config: FanCardConfig): void { + assert(config, fanCardConfigStruct); + this._config = config; + } - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - return html` - - `; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); + } + if (FAN_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.fan.${schema.name}`); } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/fan-card/fan-card.ts b/src/cards/fan-card/fan-card.ts index dc93c8c9b..3ea139248 100644 --- a/src/cards/fan-card/fan-card.ts +++ b/src/cards/fan-card/fan-card.ts @@ -1,20 +1,27 @@ import { HassEntity } from "home-assistant-js-websocket"; -import { css, CSSResultGroup, html, nothing, PropertyValues, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + nothing, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - blankBeforePercent, - computeRTL, - computeStateDisplay, - handleAction, - hasAction, - HomeAssistant, - isActive, - LovelaceCard, - LovelaceCardEditor, + actionHandler, + ActionHandlerEvent, + blankBeforePercent, + computeRTL, + computeStateDisplay, + handleAction, + hasAction, + HomeAssistant, + isActive, + LovelaceCard, + LovelaceCardEditor, } from "../../ha"; import "../../shared/badge-icon"; import "../../shared/button"; @@ -28,210 +35,226 @@ import { MushroomBaseCard } from "../../utils/base-card"; import { cardStyle } from "../../utils/card-styles"; import { registerCustomCard } from "../../utils/custom-cards"; import { computeEntityPicture } from "../../utils/info"; -import { FAN_CARD_EDITOR_NAME, FAN_CARD_NAME, FAN_ENTITY_DOMAINS } from "./const"; +import { + FAN_CARD_EDITOR_NAME, + FAN_CARD_NAME, + FAN_ENTITY_DOMAINS, +} from "./const"; import "./controls/fan-oscillate-control"; import "./controls/fan-percentage-control"; import { FanCardConfig } from "./fan-card-config"; import { getPercentage } from "./utils"; registerCustomCard({ - type: FAN_CARD_NAME, - name: "Mushroom Fan Card", - description: "Card for fan entity", + type: FAN_CARD_NAME, + name: "Mushroom Fan Card", + description: "Card for fan entity", }); @customElement(FAN_CARD_NAME) -export class FanCard extends MushroomBaseCard implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./fan-card-editor"); - return document.createElement(FAN_CARD_EDITOR_NAME) as LovelaceCardEditor; - } +export class FanCard + extends MushroomBaseCard + implements LovelaceCard +{ + public static async getConfigElement(): Promise { + await import("./fan-card-editor"); + return document.createElement(FAN_CARD_EDITOR_NAME) as LovelaceCardEditor; + } - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - const fans = entities.filter((e) => FAN_ENTITY_DOMAINS.includes(e.split(".")[0])); - return { - type: `custom:${FAN_CARD_NAME}`, - entity: fans[0], - }; - } + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + const fans = entities.filter((e) => + FAN_ENTITY_DOMAINS.includes(e.split(".")[0]) + ); + return { + type: `custom:${FAN_CARD_NAME}`, + entity: fans[0], + }; + } - protected get hasControls(): boolean { - return ( - Boolean(this._config?.show_percentage_control) || - Boolean(this._config?.show_oscillate_control) - ); - } + protected get hasControls(): boolean { + return ( + Boolean(this._config?.show_percentage_control) || + Boolean(this._config?.show_oscillate_control) + ); + } - setConfig(config: FanCardConfig): void { - super.setConfig({ - tap_action: { - action: "toggle", - }, - hold_action: { - action: "more-info", - }, - ...config, - }); - this.updatePercentage(); - } + setConfig(config: FanCardConfig): void { + super.setConfig({ + tap_action: { + action: "toggle", + }, + hold_action: { + action: "more-info", + }, + ...config, + }); + this.updatePercentage(); + } - protected updated(changedProperties: PropertyValues) { - super.updated(changedProperties); - if (this.hass && changedProperties.has("hass")) { - this.updatePercentage(); - } + protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if (this.hass && changedProperties.has("hass")) { + this.updatePercentage(); } + } - @state() - private percentage?: number; + @state() + private percentage?: number; - updatePercentage() { - this.percentage = undefined; - const stateObj = this._stateObj; - if (!this._config || !this.hass || !stateObj) return; - this.percentage = getPercentage(stateObj); - } + updatePercentage() { + this.percentage = undefined; + const stateObj = this._stateObj; + if (!this._config || !this.hass || !stateObj) return; + this.percentage = getPercentage(stateObj); + } - private onCurrentPercentageChange(e: CustomEvent<{ value?: number }>): void { - if (e.detail.value != null) { - this.percentage = Math.round(e.detail.value); - } + private onCurrentPercentageChange(e: CustomEvent<{ value?: number }>): void { + if (e.detail.value != null) { + this.percentage = Math.round(e.detail.value); } + } - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); - } + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } - protected render() { - if (!this._config || !this.hass || !this._config.entity) { - return nothing; - } + protected render() { + if (!this._config || !this.hass || !this._config.entity) { + return nothing; + } - const stateObj = this._stateObj; + const stateObj = this._stateObj; - if (!stateObj) { - return this.renderNotFound(this._config); - } + if (!stateObj) { + return this.renderNotFound(this._config); + } - const name = this._config.name || stateObj.attributes.friendly_name || ""; - const icon = this._config.icon; - const appearance = computeAppearance(this._config); - const picture = computeEntityPicture(stateObj, appearance.icon_type); - - let stateDisplay = this.hass.formatEntityState - ? this.hass.formatEntityState(stateObj) - : computeStateDisplay( - this.hass.localize, - stateObj, - this.hass.locale, - this.hass.config, - this.hass.entities - ); - if (this.percentage != null && stateObj.state === "on") { - stateDisplay = `${this.percentage}${blankBeforePercent(this.hass.locale)}%`; - } + const name = this._config.name || stateObj.attributes.friendly_name || ""; + const icon = this._config.icon; + const appearance = computeAppearance(this._config); + const picture = computeEntityPicture(stateObj, appearance.icon_type); - const rtl = computeRTL(this.hass); - - const displayControls = - (!this._config.collapsible_controls || isActive(stateObj)) && - (this._config.show_percentage_control || this._config.show_oscillate_control); - - return html` - - - - ${picture ? this.renderPicture(picture) : this.renderIcon(stateObj, icon)} - ${this.renderBadge(stateObj)} - ${this.renderStateInfo(stateObj, appearance, name, stateDisplay)}; - - ${displayControls - ? html` -
- ${this._config.show_percentage_control - ? html` - - ` - : nothing} - ${this._config.show_oscillate_control - ? html` - - ` - : nothing} -
- ` - : nothing} -
-
- `; + let stateDisplay = this.hass.formatEntityState + ? this.hass.formatEntityState(stateObj) + : computeStateDisplay( + this.hass.localize, + stateObj, + this.hass.locale, + this.hass.config, + this.hass.entities + ); + if (this.percentage != null && stateObj.state === "on") { + stateDisplay = `${this.percentage}${blankBeforePercent(this.hass.locale)}%`; } - protected renderIcon(stateObj: HassEntity, icon?: string): TemplateResult { - let iconStyle = {}; - const percentage = getPercentage(stateObj); - const active = isActive(stateObj); - if (active) { - if (percentage) { - const speed = 1.5 * (percentage / 100) ** 0.5; - iconStyle["--animation-duration"] = `${1 / speed}s`; - } else { - iconStyle["--animation-duration"] = `1s`; - } - } + const rtl = computeRTL(this.hass); - return html` - - - - `; - } + const displayControls = + (!this._config.collapsible_controls || isActive(stateObj)) && + (this._config.show_percentage_control || + this._config.show_oscillate_control); + + return html` + + + + ${picture + ? this.renderPicture(picture) + : this.renderIcon(stateObj, icon)} + ${this.renderBadge(stateObj)} + ${this.renderStateInfo(stateObj, appearance, name, stateDisplay)}; + + ${displayControls + ? html` +
+ ${this._config.show_percentage_control + ? html` + + ` + : nothing} + ${this._config.show_oscillate_control + ? html` + + ` + : nothing} +
+ ` + : nothing} +
+
+ `; + } - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - mushroom-state-item { - cursor: pointer; - } - mushroom-shape-icon { - --icon-color: rgb(var(--rgb-state-fan)); - --shape-color: rgba(var(--rgb-state-fan), 0.2); - } - .spin ha-state-icon { - animation: var(--animation-duration) infinite linear spin; - } - mushroom-fan-percentage-control { - flex: 1; - } - `, - ]; + protected renderIcon(stateObj: HassEntity, icon?: string): TemplateResult { + let iconStyle = {}; + const percentage = getPercentage(stateObj); + const active = isActive(stateObj); + if (active) { + if (percentage) { + const speed = 1.5 * (percentage / 100) ** 0.5; + iconStyle["--animation-duration"] = `${1 / speed}s`; + } else { + iconStyle["--animation-duration"] = `1s`; + } } + + return html` + + + + `; + } + + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + mushroom-state-item { + cursor: pointer; + } + mushroom-shape-icon { + --icon-color: rgb(var(--rgb-state-fan)); + --shape-color: rgba(var(--rgb-state-fan), 0.2); + } + .spin ha-state-icon { + animation: var(--animation-duration) infinite linear spin; + } + mushroom-fan-percentage-control { + flex: 1; + } + `, + ]; + } } diff --git a/src/cards/fan-card/utils.ts b/src/cards/fan-card/utils.ts index 383c3960e..8891ee9a2 100644 --- a/src/cards/fan-card/utils.ts +++ b/src/cards/fan-card/utils.ts @@ -1,20 +1,20 @@ import { HassEntity } from "home-assistant-js-websocket"; export function getPercentage(stateObj: HassEntity) { - return stateObj.attributes.percentage != null - ? Math.round(stateObj.attributes.percentage) - : undefined; + return stateObj.attributes.percentage != null + ? Math.round(stateObj.attributes.percentage) + : undefined; } export function isOscillating(stateObj: HassEntity) { - return stateObj.attributes.oscillating != null - ? Boolean(stateObj.attributes.oscillating) - : false; + return stateObj.attributes.oscillating != null + ? Boolean(stateObj.attributes.oscillating) + : false; } export function computePercentageStep(stateObj: HassEntity) { - if (stateObj.attributes.percentage_step) { - return stateObj.attributes.percentage_step; - } - return 1; + if (stateObj.attributes.percentage_step) { + return stateObj.attributes.percentage_step; + } + return 1; } diff --git a/src/cards/humidifier-card/controls/humidifier-humidity-control.ts b/src/cards/humidifier-card/controls/humidifier-humidity-control.ts index f05e07e13..e7b7ff9df 100644 --- a/src/cards/humidifier-card/controls/humidifier-humidity-control.ts +++ b/src/cards/humidifier-card/controls/humidifier-humidity-control.ts @@ -1,57 +1,62 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; -import { HomeAssistant, HumidifierEntity, isActive, isAvailable } from "../../../ha"; +import { + HomeAssistant, + HumidifierEntity, + isActive, + isAvailable, +} from "../../../ha"; import "../../../shared/slider"; @customElement("mushroom-humidifier-humidity-control") export class HumidifierHumidityControl extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public entity!: HumidifierEntity; - - @property({ attribute: false }) public color!: string | undefined; - - onChange(e: CustomEvent<{ value: number }>): void { - const value = e.detail.value; - this.hass.callService("humidifier", "set_humidity", { - entity_id: this.entity.entity_id, - humidity: value, - }); - } - - onCurrentChange(e: CustomEvent<{ value?: number }>): void { - const value = e.detail.value; - this.dispatchEvent( - new CustomEvent("current-change", { - detail: { - value, - }, - }) - ); - } - - protected render(): TemplateResult { - const max = this.entity.attributes.max_humidity || 100; - const min = this.entity.attributes.min_humidity || 0; - - return html``; - } - - static get styles(): CSSResultGroup { - return css` - mushroom-slider { - --main-color: rgb(var(--rgb-state-humidifier)); - --bg-color: rgba(var(--rgb-state-humidifier), 0.2); - } - `; - } + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public entity!: HumidifierEntity; + + @property({ attribute: false }) public color!: string | undefined; + + onChange(e: CustomEvent<{ value: number }>): void { + const value = e.detail.value; + this.hass.callService("humidifier", "set_humidity", { + entity_id: this.entity.entity_id, + humidity: value, + }); + } + + onCurrentChange(e: CustomEvent<{ value?: number }>): void { + const value = e.detail.value; + this.dispatchEvent( + new CustomEvent("current-change", { + detail: { + value, + }, + }) + ); + } + + protected render(): TemplateResult { + const max = this.entity.attributes.max_humidity || 100; + const min = this.entity.attributes.min_humidity || 0; + + return html``; + } + + static get styles(): CSSResultGroup { + return css` + mushroom-slider { + --main-color: rgb(var(--rgb-state-humidifier)); + --bg-color: rgba(var(--rgb-state-humidifier), 0.2); + } + `; + } } diff --git a/src/cards/humidifier-card/humidifier-card-config.ts b/src/cards/humidifier-card/humidifier-card-config.ts index b6b132a56..309f90c30 100644 --- a/src/cards/humidifier-card/humidifier-card-config.ts +++ b/src/cards/humidifier-card/humidifier-card-config.ts @@ -1,26 +1,36 @@ import { assign, boolean, object, optional } from "superstruct"; import { LovelaceCardConfig } from "../../ha"; -import { ActionsSharedConfig, actionsSharedConfigStruct } from "../../shared/config/actions-config"; import { - AppearanceSharedConfig, - appearanceSharedConfigStruct, + ActionsSharedConfig, + actionsSharedConfigStruct, +} from "../../shared/config/actions-config"; +import { + AppearanceSharedConfig, + appearanceSharedConfigStruct, } from "../../shared/config/appearance-config"; -import { EntitySharedConfig, entitySharedConfigStruct } from "../../shared/config/entity-config"; +import { + EntitySharedConfig, + entitySharedConfigStruct, +} from "../../shared/config/entity-config"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; export type HumidifierCardConfig = LovelaceCardConfig & - EntitySharedConfig & - AppearanceSharedConfig & - ActionsSharedConfig & { - show_target_humidity_control?: boolean; - collapsible_controls?: boolean; - }; + EntitySharedConfig & + AppearanceSharedConfig & + ActionsSharedConfig & { + show_target_humidity_control?: boolean; + collapsible_controls?: boolean; + }; export const humidifierCardConfigStruct = assign( - lovelaceCardConfigStruct, - assign(entitySharedConfigStruct, appearanceSharedConfigStruct, actionsSharedConfigStruct), - object({ - show_target_humidity_control: optional(boolean()), - collapsible_controls: optional(boolean()), - }) + lovelaceCardConfigStruct, + assign( + entitySharedConfigStruct, + appearanceSharedConfigStruct, + actionsSharedConfigStruct + ), + object({ + show_target_humidity_control: optional(boolean()), + collapsible_controls: optional(boolean()), + }) ); diff --git a/src/cards/humidifier-card/humidifier-card-editor.ts b/src/cards/humidifier-card/humidifier-card-editor.ts index 41904110f..3088f689a 100644 --- a/src/cards/humidifier-card/humidifier-card-editor.ts +++ b/src/cards/humidifier-card/humidifier-card-editor.ts @@ -9,70 +9,84 @@ import { MushroomBaseElement } from "../../utils/base-element"; import { GENERIC_LABELS } from "../../utils/form/generic-fields"; import { HaFormSchema } from "../../utils/form/ha-form"; import { loadHaComponents } from "../../utils/loader"; -import { HUMIDIFIER_CARD_EDITOR_NAME, HUMIDIFIER_ENTITY_DOMAINS } from "./const"; -import { HumidifierCardConfig, humidifierCardConfigStruct } from "./humidifier-card-config"; +import { + HUMIDIFIER_CARD_EDITOR_NAME, + HUMIDIFIER_ENTITY_DOMAINS, +} from "./const"; +import { + HumidifierCardConfig, + humidifierCardConfigStruct, +} from "./humidifier-card-config"; const HUMIDIFIER_FIELDS = ["show_target_humidity_control"]; const SCHEMA: HaFormSchema[] = [ - { name: "entity", selector: { entity: { domain: HUMIDIFIER_ENTITY_DOMAINS } } }, - { name: "name", selector: { text: {} } }, - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - ...APPEARANCE_FORM_SCHEMA, - { - type: "grid", - name: "", - schema: [ - { name: "show_target_humidity_control", selector: { boolean: {} } }, - { name: "collapsible_controls", selector: { boolean: {} } }, - ], - }, - ...computeActionsFormSchema(), + { + name: "entity", + selector: { entity: { domain: HUMIDIFIER_ENTITY_DOMAINS } }, + }, + { name: "name", selector: { text: {} } }, + { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, + ...APPEARANCE_FORM_SCHEMA, + { + type: "grid", + name: "", + schema: [ + { name: "show_target_humidity_control", selector: { boolean: {} } }, + { name: "collapsible_controls", selector: { boolean: {} } }, + ], + }, + ...computeActionsFormSchema(), ]; @customElement(HUMIDIFIER_CARD_EDITOR_NAME) -export class HumidifierCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: HumidifierCardConfig; +export class HumidifierCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: HumidifierCardConfig; - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } - - public setConfig(config: HumidifierCardConfig): void { - assert(config, humidifierCardConfigStruct); - this._config = config; - } - - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - if (HUMIDIFIER_FIELDS.includes(schema.name)) { - return customLocalize(`editor.card.humidifier.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + public setConfig(config: HumidifierCardConfig): void { + assert(config, humidifierCardConfigStruct); + this._config = config; + } - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - return html` - - `; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); } + if (HUMIDIFIER_FIELDS.includes(schema.name)) { + return customLocalize(`editor.card.humidifier.${schema.name}`); + } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/humidifier-card/humidifier-card.ts b/src/cards/humidifier-card/humidifier-card.ts index a415d9ff4..9939c3526 100644 --- a/src/cards/humidifier-card/humidifier-card.ts +++ b/src/cards/humidifier-card/humidifier-card.ts @@ -2,18 +2,18 @@ import { css, CSSResultGroup, html, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { - actionHandler, - ActionHandlerEvent, - blankBeforePercent, - computeRTL, - computeStateDisplay, - handleAction, - hasAction, - HomeAssistant, - HumidifierEntity, - isActive, - LovelaceCard, - LovelaceCardEditor, + actionHandler, + ActionHandlerEvent, + blankBeforePercent, + computeRTL, + computeStateDisplay, + handleAction, + hasAction, + HomeAssistant, + HumidifierEntity, + isActive, + LovelaceCard, + LovelaceCardEditor, } from "../../ha"; import "../../shared/badge-icon"; import "../../shared/button"; @@ -28,151 +28,159 @@ import { cardStyle } from "../../utils/card-styles"; import { registerCustomCard } from "../../utils/custom-cards"; import { computeEntityPicture } from "../../utils/info"; import { - HUMIDIFIER_CARD_EDITOR_NAME, - HUMIDIFIER_CARD_NAME, - HUMIDIFIER_ENTITY_DOMAINS, + HUMIDIFIER_CARD_EDITOR_NAME, + HUMIDIFIER_CARD_NAME, + HUMIDIFIER_ENTITY_DOMAINS, } from "./const"; import "./controls/humidifier-humidity-control"; import { HumidifierCardConfig } from "./humidifier-card-config"; registerCustomCard({ - type: HUMIDIFIER_CARD_NAME, - name: "Mushroom Humidifier Card", - description: "Card for humidifier entity", + type: HUMIDIFIER_CARD_NAME, + name: "Mushroom Humidifier Card", + description: "Card for humidifier entity", }); @customElement(HUMIDIFIER_CARD_NAME) export class HumidifierCard - extends MushroomBaseCard - implements LovelaceCard + extends MushroomBaseCard + implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./humidifier-card-editor"); - return document.createElement(HUMIDIFIER_CARD_EDITOR_NAME) as LovelaceCardEditor; + public static async getConfigElement(): Promise { + await import("./humidifier-card-editor"); + return document.createElement( + HUMIDIFIER_CARD_EDITOR_NAME + ) as LovelaceCardEditor; + } + + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + const humidifiers = entities.filter((e) => + HUMIDIFIER_ENTITY_DOMAINS.includes(e.split(".")[0]) + ); + return { + type: `custom:${HUMIDIFIER_CARD_NAME}`, + entity: humidifiers[0], + }; + } + + @state() private humidity?: number; + + protected get hasControls(): boolean { + return Boolean(this._config?.show_target_humidity_control); + } + + setConfig(config: HumidifierCardConfig): void { + super.setConfig({ + tap_action: { + action: "toggle", + }, + hold_action: { + action: "more-info", + }, + ...config, + }); + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + private onCurrentHumidityChange(e: CustomEvent<{ value?: number }>): void { + if (e.detail.value != null) { + this.humidity = e.detail.value; } + } - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - const humidifiers = entities.filter((e) => - HUMIDIFIER_ENTITY_DOMAINS.includes(e.split(".")[0]) - ); - return { - type: `custom:${HUMIDIFIER_CARD_NAME}`, - entity: humidifiers[0], - }; + protected render() { + if (!this._config || !this.hass || !this._config.entity) { + return nothing; } - @state() private humidity?: number; - - protected get hasControls(): boolean { - return Boolean(this._config?.show_target_humidity_control); - } + const stateObj = this._stateObj; - setConfig(config: HumidifierCardConfig): void { - super.setConfig({ - tap_action: { - action: "toggle", - }, - hold_action: { - action: "more-info", - }, - ...config, - }); + if (!stateObj) { + return this.renderNotFound(this._config); } - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); - } - - private onCurrentHumidityChange(e: CustomEvent<{ value?: number }>): void { - if (e.detail.value != null) { - this.humidity = e.detail.value; - } + const name = this._config.name || stateObj.attributes.friendly_name || ""; + const icon = this._config.icon; + const appearance = computeAppearance(this._config); + const picture = computeEntityPicture(stateObj, appearance.icon_type); + + let stateDisplay = this.hass.formatEntityState + ? this.hass.formatEntityState(stateObj) + : computeStateDisplay( + this.hass.localize, + stateObj, + this.hass.locale, + this.hass.config, + this.hass.entities + ); + if (this.humidity) { + stateDisplay = `${this.humidity}${blankBeforePercent(this.hass.locale)}%`; } - protected render() { - if (!this._config || !this.hass || !this._config.entity) { - return nothing; + const rtl = computeRTL(this.hass); + + const displayControls = + (!this._config.collapsible_controls || isActive(stateObj)) && + this._config.show_target_humidity_control; + + return html` + + + + ${picture + ? this.renderPicture(picture) + : this.renderIcon(stateObj, icon)} + ${this.renderBadge(stateObj)} + ${this.renderStateInfo(stateObj, appearance, name, stateDisplay)}; + + ${displayControls + ? html` +
+ +
+ ` + : nothing} +
+
+ `; + } + + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + mushroom-state-item { + cursor: pointer; } - - const stateObj = this._stateObj; - - if (!stateObj) { - return this.renderNotFound(this._config); + mushroom-shape-icon { + --icon-color: rgb(var(--rgb-state-humidifier)); + --shape-color: rgba(var(--rgb-state-humidifier), 0.2); } - - const name = this._config.name || stateObj.attributes.friendly_name || ""; - const icon = this._config.icon; - const appearance = computeAppearance(this._config); - const picture = computeEntityPicture(stateObj, appearance.icon_type); - - let stateDisplay = this.hass.formatEntityState - ? this.hass.formatEntityState(stateObj) - : computeStateDisplay( - this.hass.localize, - stateObj, - this.hass.locale, - this.hass.config, - this.hass.entities - ); - if (this.humidity) { - stateDisplay = `${this.humidity}${blankBeforePercent(this.hass.locale)}%`; + mushroom-humidifier-humidity-control { + flex: 1; } - - const rtl = computeRTL(this.hass); - - const displayControls = - (!this._config.collapsible_controls || isActive(stateObj)) && - this._config.show_target_humidity_control; - - return html` - - - - ${picture ? this.renderPicture(picture) : this.renderIcon(stateObj, icon)} - ${this.renderBadge(stateObj)} - ${this.renderStateInfo(stateObj, appearance, name, stateDisplay)}; - - ${displayControls - ? html` -
- -
- ` - : nothing} -
-
- `; - } - - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - mushroom-state-item { - cursor: pointer; - } - mushroom-shape-icon { - --icon-color: rgb(var(--rgb-state-humidifier)); - --shape-color: rgba(var(--rgb-state-humidifier), 0.2); - } - mushroom-humidifier-humidity-control { - flex: 1; - } - `, - ]; - } + `, + ]; + } } diff --git a/src/cards/light-card/controls/light-brightness-control.ts b/src/cards/light-card/controls/light-brightness-control.ts index 41158d688..b14ae5734 100644 --- a/src/cards/light-card/controls/light-brightness-control.ts +++ b/src/cards/light-card/controls/light-brightness-control.ts @@ -6,56 +6,56 @@ import { getBrightness } from "../utils"; @customElement("mushroom-light-brightness-control") export class LightBrighnessControl extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public entity!: LightEntity; + @property({ attribute: false }) public entity!: LightEntity; - onChange(e: CustomEvent<{ value: number }>): void { - const value = e.detail.value; - this.hass.callService("light", "turn_on", { - entity_id: this.entity.entity_id, - brightness_pct: value, - }); - } + onChange(e: CustomEvent<{ value: number }>): void { + const value = e.detail.value; + this.hass.callService("light", "turn_on", { + entity_id: this.entity.entity_id, + brightness_pct: value, + }); + } - onCurrentChange(e: CustomEvent<{ value?: number }>): void { - const value = e.detail.value; - this.dispatchEvent( - new CustomEvent("current-change", { - detail: { - value, - }, - }) - ); - } + onCurrentChange(e: CustomEvent<{ value?: number }>): void { + const value = e.detail.value; + this.dispatchEvent( + new CustomEvent("current-change", { + detail: { + value, + }, + }) + ); + } - protected render(): TemplateResult { - const brightness = getBrightness(this.entity); + protected render(): TemplateResult { + const brightness = getBrightness(this.entity); - return html` - - `; - } + return html` + + `; + } - static get styles(): CSSResultGroup { - return css` - :host { - --slider-color: rgb(var(--rgb-state-light)); - --slider-outline-color: transparent; - --slider-bg-color: rgba(var(--rgb-state-light), 0.2); - } - mushroom-slider { - --main-color: var(--slider-color); - --bg-color: var(--slider-bg-color); - --main-outline-color: var(--slider-outline-color); - } - `; - } + static get styles(): CSSResultGroup { + return css` + :host { + --slider-color: rgb(var(--rgb-state-light)); + --slider-outline-color: transparent; + --slider-bg-color: rgba(var(--rgb-state-light), 0.2); + } + mushroom-slider { + --main-color: var(--slider-color); + --bg-color: var(--slider-bg-color); + --main-outline-color: var(--slider-outline-color); + } + `; + } } diff --git a/src/cards/light-card/controls/light-color-control.ts b/src/cards/light-card/controls/light-color-control.ts index e6457a5bd..90b06e7a2 100644 --- a/src/cards/light-card/controls/light-color-control.ts +++ b/src/cards/light-card/controls/light-color-control.ts @@ -1,77 +1,85 @@ import * as Color from "color"; import { HassEntity } from "home-assistant-js-websocket"; -import { css, CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + TemplateResult, + unsafeCSS, +} from "lit"; import { customElement, property } from "lit/decorators.js"; import { HomeAssistant, isActive, isAvailable } from "../../../ha"; import "../../../shared/slider"; const GRADIENT = [ - [0, "#f00"], - [0.17, "#ff0"], - [0.33, "#0f0"], - [0.5, "#0ff"], - [0.66, "#00f"], - [0.83, "#f0f"], - [1, "#f00"], + [0, "#f00"], + [0.17, "#ff0"], + [0.33, "#0f0"], + [0.5, "#0ff"], + [0.66, "#00f"], + [0.83, "#f0f"], + [1, "#f00"], ]; @customElement("mushroom-light-color-control") export class LightColorControl extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public entity!: HassEntity; + @property({ attribute: false }) public entity!: HassEntity; - _percent = 0; + _percent = 0; - _percentToRGB(percent: number): number[] { - const color = Color.hsv(360 * percent, 100, 100); - return color.rgb().array(); - } + _percentToRGB(percent: number): number[] { + const color = Color.hsv(360 * percent, 100, 100); + return color.rgb().array(); + } - _rgbToPercent(rgb: number[]): number { - const color = Color.rgb(rgb); - return color.hsv().hue() / 360; - } + _rgbToPercent(rgb: number[]): number { + const color = Color.rgb(rgb); + return color.hsv().hue() / 360; + } - onChange(e: CustomEvent<{ value: number }>): void { - const value = e.detail.value; - this._percent = value; + onChange(e: CustomEvent<{ value: number }>): void { + const value = e.detail.value; + this._percent = value; - const rgb_color = this._percentToRGB(value / 100); + const rgb_color = this._percentToRGB(value / 100); - if (rgb_color.length === 3) { - this.hass.callService("light", "turn_on", { - entity_id: this.entity.entity_id, - rgb_color, - }); - } + if (rgb_color.length === 3) { + this.hass.callService("light", "turn_on", { + entity_id: this.entity.entity_id, + rgb_color, + }); } + } - protected render(): TemplateResult { - const colorPercent = - this._percent || this._rgbToPercent(this.entity.attributes.rgb_color) * 100; + protected render(): TemplateResult { + const colorPercent = + this._percent || + this._rgbToPercent(this.entity.attributes.rgb_color) * 100; - return html` - - `; - } + return html` + + `; + } - static get styles(): CSSResultGroup { - const gradient = GRADIENT.map( - ([stop, color]) => `${color} ${(stop as number) * 100}%` - ).join(", "); - return css` - mushroom-slider { - --gradient: -webkit-linear-gradient(left, ${unsafeCSS(gradient)}); - } - `; - } + static get styles(): CSSResultGroup { + const gradient = GRADIENT.map( + ([stop, color]) => `${color} ${(stop as number) * 100}%` + ).join(", "); + return css` + mushroom-slider { + --gradient: -webkit-linear-gradient(left, ${unsafeCSS(gradient)}); + } + `; + } } diff --git a/src/cards/light-card/controls/light-color-temp-control.ts b/src/cards/light-card/controls/light-color-temp-control.ts index 0c516ea5f..e608778cf 100644 --- a/src/cards/light-card/controls/light-color-temp-control.ts +++ b/src/cards/light-card/controls/light-color-temp-control.ts @@ -6,40 +6,44 @@ import { getColorTemp } from "../utils"; @customElement("mushroom-light-color-temp-control") export class LightColorTempControl extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public entity!: LightEntity; + @property({ attribute: false }) public entity!: LightEntity; - onChange(e: CustomEvent<{ value: number }>): void { - const value = e.detail.value; + onChange(e: CustomEvent<{ value: number }>): void { + const value = e.detail.value; - this.hass.callService("light", "turn_on", { - entity_id: this.entity.entity_id, - color_temp: value, - }); - } + this.hass.callService("light", "turn_on", { + entity_id: this.entity.entity_id, + color_temp: value, + }); + } - protected render(): TemplateResult { - const colorTemp = getColorTemp(this.entity); + protected render(): TemplateResult { + const colorTemp = getColorTemp(this.entity); - return html` - - `; - } + return html` + + `; + } - static get styles(): CSSResultGroup { - return css` - mushroom-slider { - --gradient: -webkit-linear-gradient(right, rgb(255, 160, 0) 0%, white 100%); - } - `; - } + static get styles(): CSSResultGroup { + return css` + mushroom-slider { + --gradient: -webkit-linear-gradient( + right, + rgb(255, 160, 0) 0%, + white 100% + ); + } + `; + } } diff --git a/src/cards/light-card/light-card-config.ts b/src/cards/light-card/light-card-config.ts index ea5f7f7f3..87ca77ff8 100644 --- a/src/cards/light-card/light-card-config.ts +++ b/src/cards/light-card/light-card-config.ts @@ -1,34 +1,44 @@ import { assign, boolean, object, optional, string } from "superstruct"; import { LovelaceCardConfig } from "../../ha"; -import { ActionsSharedConfig, actionsSharedConfigStruct } from "../../shared/config/actions-config"; import { - AppearanceSharedConfig, - appearanceSharedConfigStruct, + ActionsSharedConfig, + actionsSharedConfigStruct, +} from "../../shared/config/actions-config"; +import { + AppearanceSharedConfig, + appearanceSharedConfigStruct, } from "../../shared/config/appearance-config"; -import { EntitySharedConfig, entitySharedConfigStruct } from "../../shared/config/entity-config"; +import { + EntitySharedConfig, + entitySharedConfigStruct, +} from "../../shared/config/entity-config"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; export type LightCardConfig = LovelaceCardConfig & - EntitySharedConfig & - AppearanceSharedConfig & - ActionsSharedConfig & { - icon_color?: string; - show_brightness_control?: boolean; - show_color_temp_control?: boolean; - show_color_control?: boolean; - collapsible_controls?: boolean; - use_light_color?: boolean; - }; + EntitySharedConfig & + AppearanceSharedConfig & + ActionsSharedConfig & { + icon_color?: string; + show_brightness_control?: boolean; + show_color_temp_control?: boolean; + show_color_control?: boolean; + collapsible_controls?: boolean; + use_light_color?: boolean; + }; export const lightCardConfigStruct = assign( - lovelaceCardConfigStruct, - assign(entitySharedConfigStruct, appearanceSharedConfigStruct, actionsSharedConfigStruct), - object({ - icon_color: optional(string()), - show_brightness_control: optional(boolean()), - show_color_temp_control: optional(boolean()), - show_color_control: optional(boolean()), - collapsible_controls: optional(boolean()), - use_light_color: optional(boolean()), - }) + lovelaceCardConfigStruct, + assign( + entitySharedConfigStruct, + appearanceSharedConfigStruct, + actionsSharedConfigStruct + ), + object({ + icon_color: optional(string()), + show_brightness_control: optional(boolean()), + show_color_temp_control: optional(boolean()), + show_color_control: optional(boolean()), + collapsible_controls: optional(boolean()), + use_light_color: optional(boolean()), + }) ); diff --git a/src/cards/light-card/light-card-editor.ts b/src/cards/light-card/light-card-editor.ts index b8aac4697..c480a173f 100644 --- a/src/cards/light-card/light-card-editor.ts +++ b/src/cards/light-card/light-card-editor.ts @@ -13,81 +13,90 @@ import { LIGHT_CARD_EDITOR_NAME, LIGHT_ENTITY_DOMAINS } from "./const"; import { LightCardConfig, lightCardConfigStruct } from "./light-card-config"; export const LIGHT_LABELS = [ - "show_brightness_control", - "use_light_color", - "show_color_temp_control", - "show_color_control", + "show_brightness_control", + "use_light_color", + "show_color_temp_control", + "show_color_control", ]; const SCHEMA: HaFormSchema[] = [ - { name: "entity", selector: { entity: { domain: LIGHT_ENTITY_DOMAINS } } }, - { name: "name", selector: { text: {} } }, - { - type: "grid", - name: "", - schema: [ - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - { name: "icon_color", selector: { mush_color: {} } }, - ], - }, - ...APPEARANCE_FORM_SCHEMA, - { - type: "grid", - name: "", - schema: [ - { name: "use_light_color", selector: { boolean: {} } }, - { name: "show_brightness_control", selector: { boolean: {} } }, - { name: "show_color_temp_control", selector: { boolean: {} } }, - { name: "show_color_control", selector: { boolean: {} } }, - { name: "collapsible_controls", selector: { boolean: {} } }, - ], - }, - ...computeActionsFormSchema(), + { name: "entity", selector: { entity: { domain: LIGHT_ENTITY_DOMAINS } } }, + { name: "name", selector: { text: {} } }, + { + type: "grid", + name: "", + schema: [ + { + name: "icon", + selector: { icon: {} }, + context: { icon_entity: "entity" }, + }, + { name: "icon_color", selector: { mush_color: {} } }, + ], + }, + ...APPEARANCE_FORM_SCHEMA, + { + type: "grid", + name: "", + schema: [ + { name: "use_light_color", selector: { boolean: {} } }, + { name: "show_brightness_control", selector: { boolean: {} } }, + { name: "show_color_temp_control", selector: { boolean: {} } }, + { name: "show_color_control", selector: { boolean: {} } }, + { name: "collapsible_controls", selector: { boolean: {} } }, + ], + }, + ...computeActionsFormSchema(), ]; @customElement(LIGHT_CARD_EDITOR_NAME) -export class LightCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: LightCardConfig; +export class LightCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: LightCardConfig; - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } - - public setConfig(config: LightCardConfig): void { - assert(config, lightCardConfigStruct); - this._config = config; - } - - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - if (LIGHT_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.light.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + public setConfig(config: LightCardConfig): void { + assert(config, lightCardConfigStruct); + this._config = config; + } - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - return html` - - `; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); } + if (LIGHT_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.light.${schema.name}`); + } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/light-card/light-card.ts b/src/cards/light-card/light-card.ts index cdfe86f58..7d3a6bffd 100644 --- a/src/cards/light-card/light-card.ts +++ b/src/cards/light-card/light-card.ts @@ -1,20 +1,27 @@ -import { css, CSSResultGroup, html, nothing, PropertyValues, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + nothing, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - blankBeforePercent, - computeRTL, - computeStateDisplay, - handleAction, - hasAction, - HomeAssistant, - isActive, - LightEntity, - LovelaceCard, - LovelaceCardEditor, + actionHandler, + ActionHandlerEvent, + blankBeforePercent, + computeRTL, + computeStateDisplay, + handleAction, + hasAction, + HomeAssistant, + isActive, + LightEntity, + LovelaceCard, + LovelaceCardEditor, } from "../../ha"; import "../../shared/badge-icon"; import "../../shared/button"; @@ -29,299 +36,337 @@ import { cardStyle } from "../../utils/card-styles"; import { computeRgbColor } from "../../utils/colors"; import { registerCustomCard } from "../../utils/custom-cards"; import { computeEntityPicture } from "../../utils/info"; -import { LIGHT_CARD_EDITOR_NAME, LIGHT_CARD_NAME, LIGHT_ENTITY_DOMAINS } from "./const"; +import { + LIGHT_CARD_EDITOR_NAME, + LIGHT_CARD_NAME, + LIGHT_ENTITY_DOMAINS, +} from "./const"; import "./controls/light-brightness-control"; import "./controls/light-color-control"; import "./controls/light-color-temp-control"; import { LightCardConfig } from "./light-card-config"; import { - getBrightness, - getRGBColor, - isColorLight, - isColorSuperLight, - supportsBrightnessControl, - supportsColorControl, - supportsColorTempControl, + getBrightness, + getRGBColor, + isColorLight, + isColorSuperLight, + supportsBrightnessControl, + supportsColorControl, + supportsColorTempControl, } from "./utils"; -type LightCardControl = "brightness_control" | "color_temp_control" | "color_control"; +type LightCardControl = + | "brightness_control" + | "color_temp_control" + | "color_control"; const CONTROLS_ICONS: Record = { - brightness_control: "mdi:brightness-4", - color_temp_control: "mdi:thermometer", - color_control: "mdi:palette", + brightness_control: "mdi:brightness-4", + color_temp_control: "mdi:thermometer", + color_control: "mdi:palette", }; registerCustomCard({ - type: LIGHT_CARD_NAME, - name: "Mushroom Light Card", - description: "Card for light entity", + type: LIGHT_CARD_NAME, + name: "Mushroom Light Card", + description: "Card for light entity", }); @customElement(LIGHT_CARD_NAME) export class LightCard - extends MushroomBaseCard - implements LovelaceCard + extends MushroomBaseCard + implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./light-card-editor"); - return document.createElement(LIGHT_CARD_EDITOR_NAME) as LovelaceCardEditor; - } + public static async getConfigElement(): Promise { + await import("./light-card-editor"); + return document.createElement(LIGHT_CARD_EDITOR_NAME) as LovelaceCardEditor; + } - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - const lights = entities.filter((e) => LIGHT_ENTITY_DOMAINS.includes(e.split(".")[0])); - return { - type: `custom:${LIGHT_CARD_NAME}`, - entity: lights[0], - }; - } + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + const lights = entities.filter((e) => + LIGHT_ENTITY_DOMAINS.includes(e.split(".")[0]) + ); + return { + type: `custom:${LIGHT_CARD_NAME}`, + entity: lights[0], + }; + } - @state() private _activeControl?: LightCardControl; + @state() private _activeControl?: LightCardControl; - @state() private brightness?: number; + @state() private brightness?: number; - private get _controls(): LightCardControl[] { - if (!this._config || !this._stateObj) return []; + private get _controls(): LightCardControl[] { + if (!this._config || !this._stateObj) return []; - const stateObj = this._stateObj; - const controls: LightCardControl[] = []; - if (this._config.show_brightness_control && supportsBrightnessControl(stateObj)) { - controls.push("brightness_control"); - } - if (this._config.show_color_temp_control && supportsColorTempControl(stateObj)) { - controls.push("color_temp_control"); - } - if (this._config.show_color_control && supportsColorControl(stateObj)) { - controls.push("color_control"); - } - return controls; + const stateObj = this._stateObj; + const controls: LightCardControl[] = []; + if ( + this._config.show_brightness_control && + supportsBrightnessControl(stateObj) + ) { + controls.push("brightness_control"); } - - protected get hasControls(): boolean { - return this._controls.length > 0; + if ( + this._config.show_color_temp_control && + supportsColorTempControl(stateObj) + ) { + controls.push("color_temp_control"); } - - setConfig(config: LightCardConfig): void { - super.setConfig({ - tap_action: { - action: "toggle", - }, - hold_action: { - action: "more-info", - }, - ...config, - }); - this.updateActiveControl(); - this.updateBrightness(); + if (this._config.show_color_control && supportsColorControl(stateObj)) { + controls.push("color_control"); } + return controls; + } - _onControlTap(ctrl, e): void { - e.stopPropagation(); - this._activeControl = ctrl; - } + protected get hasControls(): boolean { + return this._controls.length > 0; + } - protected updated(changedProperties: PropertyValues) { - super.updated(changedProperties); - if (this.hass && changedProperties.has("hass")) { - this.updateActiveControl(); - this.updateBrightness(); - } - } + setConfig(config: LightCardConfig): void { + super.setConfig({ + tap_action: { + action: "toggle", + }, + hold_action: { + action: "more-info", + }, + ...config, + }); + this.updateActiveControl(); + this.updateBrightness(); + } - updateBrightness() { - this.brightness = undefined; - const stateObj = this._stateObj; + _onControlTap(ctrl, e): void { + e.stopPropagation(); + this._activeControl = ctrl; + } - if (!stateObj) return; - this.brightness = getBrightness(stateObj); + protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if (this.hass && changedProperties.has("hass")) { + this.updateActiveControl(); + this.updateBrightness(); } + } - private onCurrentBrightnessChange(e: CustomEvent<{ value?: number }>): void { - if (e.detail.value != null) { - this.brightness = e.detail.value; - } - } + updateBrightness() { + this.brightness = undefined; + const stateObj = this._stateObj; + + if (!stateObj) return; + this.brightness = getBrightness(stateObj); + } - updateActiveControl() { - const isActiveControlSupported = this._activeControl - ? this._controls.includes(this._activeControl) - : false; - this._activeControl = isActiveControlSupported ? this._activeControl : this._controls[0]; + private onCurrentBrightnessChange(e: CustomEvent<{ value?: number }>): void { + if (e.detail.value != null) { + this.brightness = e.detail.value; } + } - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); + updateActiveControl() { + const isActiveControlSupported = this._activeControl + ? this._controls.includes(this._activeControl) + : false; + this._activeControl = isActiveControlSupported + ? this._activeControl + : this._controls[0]; + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + protected render() { + if (!this._config || !this.hass || !this._config.entity) { + return nothing; } - protected render() { - if (!this._config || !this.hass || !this._config.entity) { - return nothing; - } + const stateObj = this._stateObj; - const stateObj = this._stateObj; + if (!stateObj) { + return this.renderNotFound(this._config); + } - if (!stateObj) { - return this.renderNotFound(this._config); - } + const name = this._config.name || stateObj.attributes.friendly_name || ""; + const icon = this._config.icon; + const appearance = computeAppearance(this._config); + const picture = computeEntityPicture(stateObj, appearance.icon_type); - const name = this._config.name || stateObj.attributes.friendly_name || ""; - const icon = this._config.icon; - const appearance = computeAppearance(this._config); - const picture = computeEntityPicture(stateObj, appearance.icon_type); + let stateDisplay = this.hass.formatEntityState + ? this.hass.formatEntityState(stateObj) + : computeStateDisplay( + this.hass.localize, + stateObj, + this.hass.locale, + this.hass.config, + this.hass.entities + ); + if (this.brightness != null) { + stateDisplay = `${this.brightness}${blankBeforePercent(this.hass.locale)}%`; + } - let stateDisplay = this.hass.formatEntityState - ? this.hass.formatEntityState(stateObj) - : computeStateDisplay( - this.hass.localize, - stateObj, - this.hass.locale, - this.hass.config, - this.hass.entities - ); - if (this.brightness != null) { - stateDisplay = `${this.brightness}${blankBeforePercent(this.hass.locale)}%`; - } + const rtl = computeRTL(this.hass); - const rtl = computeRTL(this.hass); + const isControlVisible = + (!this._config.collapsible_controls || isActive(stateObj)) && + this._controls.length; - const isControlVisible = - (!this._config.collapsible_controls || isActive(stateObj)) && this._controls.length; + return html` + + + + ${picture + ? this.renderPicture(picture) + : this.renderIcon(stateObj, icon)} + ${this.renderBadge(stateObj)} + ${this.renderStateInfo(stateObj, appearance, name, stateDisplay)}; + + ${isControlVisible + ? html` +
+ ${this.renderActiveControl(stateObj)} + ${this.renderOtherControls()} +
+ ` + : nothing} +
+
+ `; + } - return html` - - - - ${picture ? this.renderPicture(picture) : this.renderIcon(stateObj, icon)} - ${this.renderBadge(stateObj)} - ${this.renderStateInfo(stateObj, appearance, name, stateDisplay)}; - - ${isControlVisible - ? html` -
- ${this.renderActiveControl(stateObj)} - ${this.renderOtherControls()} -
- ` - : nothing} -
-
- `; + protected renderIcon(stateObj: LightEntity, icon?: string): TemplateResult { + const lightRgbColor = getRGBColor(stateObj); + const active = isActive(stateObj); + const iconStyle = {}; + const iconColor = this._config?.icon_color; + if (lightRgbColor && this._config?.use_light_color) { + const color = lightRgbColor.join(","); + iconStyle["--icon-color"] = `rgb(${color})`; + iconStyle["--shape-color"] = `rgba(${color}, 0.25)`; + if (isColorLight(lightRgbColor) && !(this.hass.themes as any).darkMode) { + iconStyle["--shape-outline-color"] = + `rgba(var(--rgb-primary-text-color), 0.05)`; + if (isColorSuperLight(lightRgbColor)) { + iconStyle["--icon-color"] = + `rgba(var(--rgb-primary-text-color), 0.2)`; + } + } + } else if (iconColor) { + const iconRgbColor = computeRgbColor(iconColor); + iconStyle["--icon-color"] = `rgb(${iconRgbColor})`; + iconStyle["--shape-color"] = `rgba(${iconRgbColor}, 0.2)`; } + return html` + + + + `; + } + + private renderOtherControls(): TemplateResult | null { + const otherControls = this._controls.filter( + (control) => control != this._activeControl + ); - protected renderIcon(stateObj: LightEntity, icon?: string): TemplateResult { - const lightRgbColor = getRGBColor(stateObj); - const active = isActive(stateObj); - const iconStyle = {}; + return html` + ${otherControls.map( + (ctrl) => html` + this._onControlTap(ctrl, e)}> + + + ` + )} + `; + } + + private renderActiveControl(entity: LightEntity) { + switch (this._activeControl) { + case "brightness_control": + const lightRgbColor = getRGBColor(entity); + const sliderStyle = {}; const iconColor = this._config?.icon_color; if (lightRgbColor && this._config?.use_light_color) { - const color = lightRgbColor.join(","); - iconStyle["--icon-color"] = `rgb(${color})`; - iconStyle["--shape-color"] = `rgba(${color}, 0.25)`; - if (isColorLight(lightRgbColor) && !(this.hass.themes as any).darkMode) { - iconStyle["--shape-outline-color"] = `rgba(var(--rgb-primary-text-color), 0.05)`; - if (isColorSuperLight(lightRgbColor)) { - iconStyle["--icon-color"] = `rgba(var(--rgb-primary-text-color), 0.2)`; - } - } + const color = lightRgbColor.join(","); + sliderStyle["--slider-color"] = `rgb(${color})`; + sliderStyle["--slider-bg-color"] = `rgba(${color}, 0.2)`; + if ( + isColorLight(lightRgbColor) && + !(this.hass.themes as any).darkMode + ) { + sliderStyle["--slider-bg-color"] = + `rgba(var(--rgb-primary-text-color), 0.05)`; + sliderStyle["--slider-color"] = + `rgba(var(--rgb-primary-text-color), 0.15)`; + } } else if (iconColor) { - const iconRgbColor = computeRgbColor(iconColor); - iconStyle["--icon-color"] = `rgb(${iconRgbColor})`; - iconStyle["--shape-color"] = `rgba(${iconRgbColor}, 0.2)`; + const iconRgbColor = computeRgbColor(iconColor); + sliderStyle["--slider-color"] = `rgb(${iconRgbColor})`; + sliderStyle["--slider-bg-color"] = `rgba(${iconRgbColor}, 0.2)`; } return html` - - - + `; - } - - private renderOtherControls(): TemplateResult | null { - const otherControls = this._controls.filter((control) => control != this._activeControl); - + case "color_temp_control": return html` - ${otherControls.map( - (ctrl) => html` - this._onControlTap(ctrl, e)}> - - - ` - )} + `; + case "color_control": + return html` + + `; + default: + return nothing; } + } - private renderActiveControl(entity: LightEntity) { - switch (this._activeControl) { - case "brightness_control": - const lightRgbColor = getRGBColor(entity); - const sliderStyle = {}; - const iconColor = this._config?.icon_color; - if (lightRgbColor && this._config?.use_light_color) { - const color = lightRgbColor.join(","); - sliderStyle["--slider-color"] = `rgb(${color})`; - sliderStyle["--slider-bg-color"] = `rgba(${color}, 0.2)`; - if (isColorLight(lightRgbColor) && !(this.hass.themes as any).darkMode) { - sliderStyle[ - "--slider-bg-color" - ] = `rgba(var(--rgb-primary-text-color), 0.05)`; - sliderStyle["--slider-color"] = `rgba(var(--rgb-primary-text-color), 0.15)`; - } - } else if (iconColor) { - const iconRgbColor = computeRgbColor(iconColor); - sliderStyle["--slider-color"] = `rgb(${iconRgbColor})`; - sliderStyle["--slider-bg-color"] = `rgba(${iconRgbColor}, 0.2)`; - } - return html` - - `; - case "color_temp_control": - return html` - - `; - case "color_control": - return html` - - `; - default: - return nothing; + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + mushroom-state-item { + cursor: pointer; } - } - - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - mushroom-state-item { - cursor: pointer; - } - mushroom-shape-icon { - --icon-color: rgb(var(--rgb-state-light)); - --shape-color: rgba(var(--rgb-state-light), 0.2); - } - mushroom-light-brightness-control, - mushroom-light-color-temp-control, - mushroom-light-color-control { - flex: 1; - } - `, - ]; - } + mushroom-shape-icon { + --icon-color: rgb(var(--rgb-state-light)); + --shape-color: rgba(var(--rgb-state-light), 0.2); + } + mushroom-light-brightness-control, + mushroom-light-color-temp-control, + mushroom-light-color-control { + flex: 1; + } + `, + ]; + } } diff --git a/src/cards/light-card/utils.ts b/src/cards/light-card/utils.ts index eb60f4f25..76102c3bc 100644 --- a/src/cards/light-card/utils.ts +++ b/src/cards/light-card/utils.ts @@ -1,43 +1,50 @@ import * as Color from "color"; -import { LightColorMode, LightEntity, lightSupportsColor, lightSupportsBrightness } from "../../ha"; +import { + LightColorMode, + LightEntity, + lightSupportsColor, + lightSupportsBrightness, +} from "../../ha"; export function getBrightness(entity: LightEntity): number | undefined { - return entity.attributes.brightness != null - ? Math.max(Math.round((entity.attributes.brightness * 100) / 255), 1) - : undefined; + return entity.attributes.brightness != null + ? Math.max(Math.round((entity.attributes.brightness * 100) / 255), 1) + : undefined; } export function getColorTemp(entity: LightEntity): number | undefined { - return entity.attributes.color_temp != null - ? Math.round(entity.attributes.color_temp) - : undefined; + return entity.attributes.color_temp != null + ? Math.round(entity.attributes.color_temp) + : undefined; } export function getRGBColor(entity: LightEntity): number[] | undefined { - return entity.attributes.rgb_color != null ? entity.attributes.rgb_color : undefined; + return entity.attributes.rgb_color != null + ? entity.attributes.rgb_color + : undefined; } export function isColorLight(rgb: number[]): boolean { - const color = Color.rgb(rgb); - return color.l() > 96; + const color = Color.rgb(rgb); + return color.l() > 96; } export function isColorSuperLight(rgb: number[]): boolean { - const color = Color.rgb(rgb); - return color.l() > 97; + const color = Color.rgb(rgb); + return color.l() > 97; } export function supportsColorTempControl(entity: LightEntity): boolean { - return ( - entity.attributes.supported_color_modes?.some((m) => - [LightColorMode.COLOR_TEMP].includes(m) - ) ?? false - ); + return ( + entity.attributes.supported_color_modes?.some((m) => + [LightColorMode.COLOR_TEMP].includes(m) + ) ?? false + ); } export function supportsColorControl(entity: LightEntity): boolean { - return lightSupportsColor(entity); + return lightSupportsColor(entity); } export function supportsBrightnessControl(entity: LightEntity): boolean { - return lightSupportsBrightness(entity); + return lightSupportsBrightness(entity); } diff --git a/src/cards/lock-card/controls/lock-buttons-control.ts b/src/cards/lock-card/controls/lock-buttons-control.ts index 708a9f0d7..2135a502f 100644 --- a/src/cards/lock-card/controls/lock-buttons-control.ts +++ b/src/cards/lock-card/controls/lock-buttons-control.ts @@ -1,90 +1,92 @@ import { html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; import { - computeRTL, - HomeAssistant, - isAvailable, - LockEntity, - LOCK_SUPPORT_OPEN, - supportsFeature, + computeRTL, + HomeAssistant, + isAvailable, + LockEntity, + LOCK_SUPPORT_OPEN, + supportsFeature, } from "../../../ha"; import setupCustomlocalize from "../../../localize"; import { isActionPending, isLocked, isUnlocked } from "../utils"; interface LockButton { - icon: string; - title?: string; - serviceName?: string; - isVisible: (entity: LockEntity) => boolean; - isDisabled: (entity: LockEntity) => boolean; + icon: string; + title?: string; + serviceName?: string; + isVisible: (entity: LockEntity) => boolean; + isDisabled: (entity: LockEntity) => boolean; } export const LOCK_BUTTONS: LockButton[] = [ - { - icon: "mdi:lock", - title: "lock", - serviceName: "lock", - isVisible: (entity) => isUnlocked(entity), - isDisabled: () => false, - }, - { - icon: "mdi:lock-open", - title: "unlock", - serviceName: "unlock", - isVisible: (entity) => isLocked(entity), - isDisabled: () => false, - }, - { - icon: "mdi:lock-clock", - isVisible: (entity) => isActionPending(entity), - isDisabled: () => true, - }, - { - icon: "mdi:door-open", - title: "open", - serviceName: "open", - isVisible: (entity) => supportsFeature(entity, LOCK_SUPPORT_OPEN) && isUnlocked(entity), - isDisabled: (entity) => isActionPending(entity), - }, + { + icon: "mdi:lock", + title: "lock", + serviceName: "lock", + isVisible: (entity) => isUnlocked(entity), + isDisabled: () => false, + }, + { + icon: "mdi:lock-open", + title: "unlock", + serviceName: "unlock", + isVisible: (entity) => isLocked(entity), + isDisabled: () => false, + }, + { + icon: "mdi:lock-clock", + isVisible: (entity) => isActionPending(entity), + isDisabled: () => true, + }, + { + icon: "mdi:door-open", + title: "open", + serviceName: "open", + isVisible: (entity) => + supportsFeature(entity, LOCK_SUPPORT_OPEN) && isUnlocked(entity), + isDisabled: (entity) => isActionPending(entity), + }, ]; @customElement("mushroom-lock-buttons-control") export class LockButtonsControl extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public entity!: LockEntity; + @property({ attribute: false }) public entity!: LockEntity; - @property({ type: Boolean }) public fill: boolean = false; + @property({ type: Boolean }) public fill: boolean = false; - private callService(e: CustomEvent) { - e.stopPropagation(); - const entry = (e.target! as any).entry as LockButton; - this.hass.callService("lock", entry.serviceName!, { - entity_id: this.entity!.entity_id, - }); - } + private callService(e: CustomEvent) { + e.stopPropagation(); + const entry = (e.target! as any).entry as LockButton; + this.hass.callService("lock", entry.serviceName!, { + entity_id: this.entity!.entity_id, + }); + } - protected render(): TemplateResult { - const rtl = computeRTL(this.hass); - const customLocalize = setupCustomlocalize(this.hass!); + protected render(): TemplateResult { + const rtl = computeRTL(this.hass); + const customLocalize = setupCustomlocalize(this.hass!); - return html` - ${LOCK_BUTTONS.filter((item) => item.isVisible(this.entity)).map( - (item) => html` - - - - ` - )}${LOCK_BUTTONS.filter((item) => item.isVisible(this.entity)).map( + (item) => html` + - `; - } + + + ` + )} + `; + } } diff --git a/src/cards/lock-card/lock-card-config.ts b/src/cards/lock-card/lock-card-config.ts index b431a5a12..685e8da08 100644 --- a/src/cards/lock-card/lock-card-config.ts +++ b/src/cards/lock-card/lock-card-config.ts @@ -1,19 +1,29 @@ import { assign, boolean, object, optional } from "superstruct"; import { LovelaceCardConfig } from "../../ha"; -import { ActionsSharedConfig, actionsSharedConfigStruct } from "../../shared/config/actions-config"; import { - AppearanceSharedConfig, - appearanceSharedConfigStruct, + ActionsSharedConfig, + actionsSharedConfigStruct, +} from "../../shared/config/actions-config"; +import { + AppearanceSharedConfig, + appearanceSharedConfigStruct, } from "../../shared/config/appearance-config"; -import { EntitySharedConfig, entitySharedConfigStruct } from "../../shared/config/entity-config"; +import { + EntitySharedConfig, + entitySharedConfigStruct, +} from "../../shared/config/entity-config"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; export type LockCardConfig = LovelaceCardConfig & - EntitySharedConfig & - AppearanceSharedConfig & - ActionsSharedConfig; + EntitySharedConfig & + AppearanceSharedConfig & + ActionsSharedConfig; export const lockCardConfigStruct = assign( - lovelaceCardConfigStruct, - assign(entitySharedConfigStruct, appearanceSharedConfigStruct, actionsSharedConfigStruct) + lovelaceCardConfigStruct, + assign( + entitySharedConfigStruct, + appearanceSharedConfigStruct, + actionsSharedConfigStruct + ) ); diff --git a/src/cards/lock-card/lock-card-editor.ts b/src/cards/lock-card/lock-card-editor.ts index 854f14b4a..48ffc2e40 100644 --- a/src/cards/lock-card/lock-card-editor.ts +++ b/src/cards/lock-card/lock-card-editor.ts @@ -13,53 +13,58 @@ import { LOCK_CARD_EDITOR_NAME, LOCK_ENTITY_DOMAINS } from "./const"; import { LockCardConfig, lockCardConfigStruct } from "./lock-card-config"; const SCHEMA: HaFormSchema[] = [ - { name: "entity", selector: { entity: { domain: LOCK_ENTITY_DOMAINS } } }, - { name: "name", selector: { text: {} } }, - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - ...APPEARANCE_FORM_SCHEMA, - ...computeActionsFormSchema(), + { name: "entity", selector: { entity: { domain: LOCK_ENTITY_DOMAINS } } }, + { name: "name", selector: { text: {} } }, + { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, + ...APPEARANCE_FORM_SCHEMA, + ...computeActionsFormSchema(), ]; @customElement(LOCK_CARD_EDITOR_NAME) -export class LockCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: LockCardConfig; +export class LockCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: LockCardConfig; - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } - - public setConfig(config: LockCardConfig): void { - assert(config, lockCardConfigStruct); - this._config = config; - } + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + public setConfig(config: LockCardConfig): void { + assert(config, lockCardConfigStruct); + this._config = config; + } - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - protected render() { - if (!this.hass || !this._config) { - return nothing; - } - - return html` - - `; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/lock-card/lock-card.ts b/src/cards/lock-card/lock-card.ts index a9e7c4e54..8011a6b6a 100644 --- a/src/cards/lock-card/lock-card.ts +++ b/src/cards/lock-card/lock-card.ts @@ -3,16 +3,16 @@ import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - computeRTL, - handleAction, - hasAction, - HomeAssistant, - isAvailable, - LockEntity, - LovelaceCard, - LovelaceCardEditor, + actionHandler, + ActionHandlerEvent, + computeRTL, + handleAction, + hasAction, + HomeAssistant, + isAvailable, + LockEntity, + LovelaceCard, + LovelaceCardEditor, } from "../../ha"; import "../../shared/badge-icon"; import "../../shared/button"; @@ -25,132 +25,149 @@ import { MushroomBaseCard } from "../../utils/base-card"; import { cardStyle } from "../../utils/card-styles"; import { registerCustomCard } from "../../utils/custom-cards"; import { computeEntityPicture } from "../../utils/info"; -import { LOCK_CARD_EDITOR_NAME, LOCK_CARD_NAME, LOCK_ENTITY_DOMAINS } from "./const"; +import { + LOCK_CARD_EDITOR_NAME, + LOCK_CARD_NAME, + LOCK_ENTITY_DOMAINS, +} from "./const"; import "./controls/lock-buttons-control"; import { LockCardConfig } from "./lock-card-config"; import { isActionPending, isLocked, isUnlocked } from "./utils"; registerCustomCard({ - type: LOCK_CARD_NAME, - name: "Mushroom Lock Card", - description: "Card for all lock entities", + type: LOCK_CARD_NAME, + name: "Mushroom Lock Card", + description: "Card for all lock entities", }); @customElement(LOCK_CARD_NAME) -export class LockCard extends MushroomBaseCard implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./lock-card-editor"); - return document.createElement(LOCK_CARD_EDITOR_NAME) as LovelaceCardEditor; +export class LockCard + extends MushroomBaseCard + implements LovelaceCard +{ + public static async getConfigElement(): Promise { + await import("./lock-card-editor"); + return document.createElement(LOCK_CARD_EDITOR_NAME) as LovelaceCardEditor; + } + + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + const locks = entities.filter((e) => + LOCK_ENTITY_DOMAINS.includes(e.split(".")[0]) + ); + return { + type: `custom:${LOCK_CARD_NAME}`, + entity: locks[0], + }; + } + + protected get hasControls(): boolean { + return true; + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + protected render() { + if (!this._config || !this.hass || !this._config.entity) { + return nothing; } - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - const locks = entities.filter((e) => LOCK_ENTITY_DOMAINS.includes(e.split(".")[0])); - return { - type: `custom:${LOCK_CARD_NAME}`, - entity: locks[0], - }; - } + const stateObj = this._stateObj; - protected get hasControls(): boolean { - return true; + if (!stateObj) { + return this.renderNotFound(this._config); } - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); + const name = this._config.name || stateObj.attributes.friendly_name || ""; + const icon = this._config.icon; + const appearance = computeAppearance(this._config); + const picture = computeEntityPicture(stateObj, appearance.icon_type); + + const rtl = computeRTL(this.hass); + + return html` + + + + ${picture + ? this.renderPicture(picture) + : this.renderIcon(stateObj as LockEntity, icon)} + ${this.renderBadge(stateObj)} + ${this.renderStateInfo(stateObj, appearance, name)}; + +
+ + +
+
+
+ `; + } + + renderIcon(stateObj: LockEntity, icon?: string): TemplateResult { + const available = isAvailable(stateObj); + + const iconStyle = { + "--icon-color": "rgb(var(--rgb-state-lock))", + "--shape-color": "rgba(var(--rgb-state-lock), 0.2)", + }; + + if (isLocked(stateObj)) { + iconStyle["--icon-color"] = `rgb(var(--rgb-state-lock-locked))`; + iconStyle["--shape-color"] = `rgba(var(--rgb-state-lock-locked), 0.2)`; + } else if (isUnlocked(stateObj)) { + iconStyle["--icon-color"] = `rgb(var(--rgb-state-lock-unlocked))`; + iconStyle["--shape-color"] = `rgba(var(--rgb-state-lock-unlocked), 0.2)`; + } else if (isActionPending(stateObj)) { + iconStyle["--icon-color"] = `rgb(var(--rgb-state-lock-pending))`; + iconStyle["--shape-color"] = `rgba(var(--rgb-state-lock-pending), 0.2)`; } - protected render() { - if (!this._config || !this.hass || !this._config.entity) { - return nothing; - } - - const stateObj = this._stateObj; - - if (!stateObj) { - return this.renderNotFound(this._config); + return html` + + + + `; + } + + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + mushroom-state-item { + cursor: pointer; } - - const name = this._config.name || stateObj.attributes.friendly_name || ""; - const icon = this._config.icon; - const appearance = computeAppearance(this._config); - const picture = computeEntityPicture(stateObj, appearance.icon_type); - - const rtl = computeRTL(this.hass); - - return html` - - - - ${picture - ? this.renderPicture(picture) - : this.renderIcon(stateObj as LockEntity, icon)} - ${this.renderBadge(stateObj)} - ${this.renderStateInfo(stateObj, appearance, name)}; - -
- - -
-
-
- `; - } - - renderIcon(stateObj: LockEntity, icon?: string): TemplateResult { - const available = isAvailable(stateObj); - - const iconStyle = { - "--icon-color": "rgb(var(--rgb-state-lock))", - "--shape-color": "rgba(var(--rgb-state-lock), 0.2)", - }; - - if (isLocked(stateObj)) { - iconStyle["--icon-color"] = `rgb(var(--rgb-state-lock-locked))`; - iconStyle["--shape-color"] = `rgba(var(--rgb-state-lock-locked), 0.2)`; - } else if (isUnlocked(stateObj)) { - iconStyle["--icon-color"] = `rgb(var(--rgb-state-lock-unlocked))`; - iconStyle["--shape-color"] = `rgba(var(--rgb-state-lock-unlocked), 0.2)`; - } else if (isActionPending(stateObj)) { - iconStyle["--icon-color"] = `rgb(var(--rgb-state-lock-pending))`; - iconStyle["--shape-color"] = `rgba(var(--rgb-state-lock-pending), 0.2)`; + mushroom-lock-buttons-control { + flex: 1; } - - return html` - - - - `; - } - - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - mushroom-state-item { - cursor: pointer; - } - mushroom-lock-buttons-control { - flex: 1; - } - `, - ]; - } + `, + ]; + } } diff --git a/src/cards/lock-card/utils.ts b/src/cards/lock-card/utils.ts index 65fc99db8..4a74fbf5d 100644 --- a/src/cards/lock-card/utils.ts +++ b/src/cards/lock-card/utils.ts @@ -1,25 +1,25 @@ import { - LockEntity, - LOCK_STATE_LOCKED, - LOCK_STATE_LOCKING, - LOCK_STATE_UNLOCKED, - LOCK_STATE_UNLOCKING, + LockEntity, + LOCK_STATE_LOCKED, + LOCK_STATE_LOCKING, + LOCK_STATE_UNLOCKED, + LOCK_STATE_UNLOCKING, } from "../../ha"; export function isUnlocked(entity: LockEntity) { - return entity.state === LOCK_STATE_UNLOCKED; + return entity.state === LOCK_STATE_UNLOCKED; } export function isLocked(entity: LockEntity) { - return entity.state === LOCK_STATE_LOCKED; + return entity.state === LOCK_STATE_LOCKED; } export function isActionPending(entity: LockEntity) { - switch (entity.state) { - case LOCK_STATE_LOCKING: - case LOCK_STATE_UNLOCKING: - return true; - default: - return false; - } + switch (entity.state) { + case LOCK_STATE_LOCKING: + case LOCK_STATE_UNLOCKING: + return true; + default: + return false; + } } diff --git a/src/cards/media-player-card/controls/media-player-media-control.ts b/src/cards/media-player-card/controls/media-player-media-control.ts index 8e8b3a5f9..fb6226272 100644 --- a/src/cards/media-player-card/controls/media-player-media-control.ts +++ b/src/cards/media-player-card/controls/media-player-media-control.ts @@ -5,41 +5,44 @@ import { MediaPlayerMediaControl } from "../media-player-card-config"; import { computeMediaControls, handleMediaControlClick } from "../utils"; export const isMediaControlVisible = ( - entity: MediaPlayerEntity, - controls?: MediaPlayerMediaControl[] + entity: MediaPlayerEntity, + controls?: MediaPlayerMediaControl[] ) => computeMediaControls(entity, controls ?? []).length > 0; @customElement("mushroom-media-player-media-control") export class MediaPlayerMediaControls extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public entity!: MediaPlayerEntity; - - @property({ attribute: false }) public controls!: MediaPlayerMediaControl[]; - - @property({ type: Boolean }) public fill: boolean = false; - - private _handleClick(e: MouseEvent): void { - e.stopPropagation(); - const action = (e.target! as any).action as string; - handleMediaControlClick(this.hass, this.entity, action!); - } - - protected render(): TemplateResult { - const rtl = computeRTL(this.hass); - - const controls = computeMediaControls(this.entity, this.controls); - - return html` - - ${controls.map( - (control) => html` - - - - ` - )} - - `; - } + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public entity!: MediaPlayerEntity; + + @property({ attribute: false }) public controls!: MediaPlayerMediaControl[]; + + @property({ type: Boolean }) public fill: boolean = false; + + private _handleClick(e: MouseEvent): void { + e.stopPropagation(); + const action = (e.target! as any).action as string; + handleMediaControlClick(this.hass, this.entity, action!); + } + + protected render(): TemplateResult { + const rtl = computeRTL(this.hass); + + const controls = computeMediaControls(this.entity, this.controls); + + return html` + + ${controls.map( + (control) => html` + + + + ` + )} + + `; + } } diff --git a/src/cards/media-player-card/controls/media-player-volume-control.ts b/src/cards/media-player-card/controls/media-player-volume-control.ts index 5c9f4ca24..4116d0b35 100644 --- a/src/cards/media-player-card/controls/media-player-volume-control.ts +++ b/src/cards/media-player-card/controls/media-player-volume-control.ts @@ -1,146 +1,150 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators.js"; import { - computeRTL, - HomeAssistant, - isActive, - isAvailable, - isOff, - MEDIA_PLAYER_SUPPORT_VOLUME_BUTTONS, - MEDIA_PLAYER_SUPPORT_VOLUME_MUTE, - MEDIA_PLAYER_SUPPORT_VOLUME_SET, - MediaPlayerEntity, - supportsFeature, + computeRTL, + HomeAssistant, + isActive, + isAvailable, + isOff, + MEDIA_PLAYER_SUPPORT_VOLUME_BUTTONS, + MEDIA_PLAYER_SUPPORT_VOLUME_MUTE, + MEDIA_PLAYER_SUPPORT_VOLUME_SET, + MediaPlayerEntity, + supportsFeature, } from "../../../ha"; import { MediaPlayerVolumeControl } from "../media-player-card-config"; import { getVolumeLevel, handleMediaControlClick } from "../utils"; export const isVolumeControlVisible = ( - entity: MediaPlayerEntity, - controls?: MediaPlayerVolumeControl[] + entity: MediaPlayerEntity, + controls?: MediaPlayerVolumeControl[] ) => - (controls?.includes("volume_buttons") && - supportsFeature(entity, MEDIA_PLAYER_SUPPORT_VOLUME_BUTTONS)) || - (controls?.includes("volume_mute") && - supportsFeature(entity, MEDIA_PLAYER_SUPPORT_VOLUME_MUTE)) || - (controls?.includes("volume_set") && supportsFeature(entity, MEDIA_PLAYER_SUPPORT_VOLUME_SET)); + (controls?.includes("volume_buttons") && + supportsFeature(entity, MEDIA_PLAYER_SUPPORT_VOLUME_BUTTONS)) || + (controls?.includes("volume_mute") && + supportsFeature(entity, MEDIA_PLAYER_SUPPORT_VOLUME_MUTE)) || + (controls?.includes("volume_set") && + supportsFeature(entity, MEDIA_PLAYER_SUPPORT_VOLUME_SET)); @customElement("mushroom-media-player-volume-control") export class MediaPlayerVolumeControls extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public entity!: MediaPlayerEntity; - - @property({ type: Boolean }) public fill: boolean = false; - - @property({ attribute: false }) public controls!: MediaPlayerVolumeControl[]; - - private handleSliderChange(e: CustomEvent<{ value: number }>): void { - const value = e.detail.value; - this.hass.callService("media_player", "volume_set", { - entity_id: this.entity.entity_id, - volume_level: value / 100, - }); - } - - handleSliderCurrentChange(e: CustomEvent<{ value?: number }>): void { - let value = e.detail.value; - this.dispatchEvent( - new CustomEvent("current-change", { - detail: { - value, - }, - }) - ); - } - - private handleClick(e: MouseEvent): void { - e.stopPropagation(); - const action = (e.target! as any).action as string; - handleMediaControlClick(this.hass, this.entity, action!); - } - - protected render() { - if (!this.entity) return nothing; - - const value = getVolumeLevel(this.entity); - - const rtl = computeRTL(this.hass); - - const displayVolumeSet = - this.controls?.includes("volume_set") && - supportsFeature(this.entity, MEDIA_PLAYER_SUPPORT_VOLUME_SET); - - const displayVolumeMute = - this.controls?.includes("volume_mute") && - supportsFeature(this.entity, MEDIA_PLAYER_SUPPORT_VOLUME_MUTE); - - const displayVolumeButtons = - this.controls?.includes("volume_buttons") && - supportsFeature(this.entity, MEDIA_PLAYER_SUPPORT_VOLUME_BUTTONS); - - return html` - - ${displayVolumeSet - ? html` ` - : nothing} - ${displayVolumeMute - ? html` - - - - ` - : undefined} - ${displayVolumeButtons - ? html` - - - ` - : undefined} - ${displayVolumeButtons - ? html` - - - ` - : undefined} - - `; - } - - static get styles(): CSSResultGroup { - return css` - mushroom-slider { - flex: 1; - --main-color: rgb(var(--rgb-state-media-player)); - --bg-color: rgba(var(--rgb-state-media-player), 0.2); - } - `; - } + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public entity!: MediaPlayerEntity; + + @property({ type: Boolean }) public fill: boolean = false; + + @property({ attribute: false }) public controls!: MediaPlayerVolumeControl[]; + + private handleSliderChange(e: CustomEvent<{ value: number }>): void { + const value = e.detail.value; + this.hass.callService("media_player", "volume_set", { + entity_id: this.entity.entity_id, + volume_level: value / 100, + }); + } + + handleSliderCurrentChange(e: CustomEvent<{ value?: number }>): void { + let value = e.detail.value; + this.dispatchEvent( + new CustomEvent("current-change", { + detail: { + value, + }, + }) + ); + } + + private handleClick(e: MouseEvent): void { + e.stopPropagation(); + const action = (e.target! as any).action as string; + handleMediaControlClick(this.hass, this.entity, action!); + } + + protected render() { + if (!this.entity) return nothing; + + const value = getVolumeLevel(this.entity); + + const rtl = computeRTL(this.hass); + + const displayVolumeSet = + this.controls?.includes("volume_set") && + supportsFeature(this.entity, MEDIA_PLAYER_SUPPORT_VOLUME_SET); + + const displayVolumeMute = + this.controls?.includes("volume_mute") && + supportsFeature(this.entity, MEDIA_PLAYER_SUPPORT_VOLUME_MUTE); + + const displayVolumeButtons = + this.controls?.includes("volume_buttons") && + supportsFeature(this.entity, MEDIA_PLAYER_SUPPORT_VOLUME_BUTTONS); + + return html` + + ${displayVolumeSet + ? html` ` + : nothing} + ${displayVolumeMute + ? html` + + + + ` + : undefined} + ${displayVolumeButtons + ? html` + + + ` + : undefined} + ${displayVolumeButtons + ? html` + + + ` + : undefined} + + `; + } + + static get styles(): CSSResultGroup { + return css` + mushroom-slider { + flex: 1; + --main-color: rgb(var(--rgb-state-media-player)); + --bg-color: rgba(var(--rgb-state-media-player), 0.2); + } + `; + } } diff --git a/src/cards/media-player-card/media-player-card-config.ts b/src/cards/media-player-card/media-player-card-config.ts index 07bcaea7c..b6f04863c 100644 --- a/src/cards/media-player-card/media-player-card-config.ts +++ b/src/cards/media-player-card/media-player-card-config.ts @@ -1,51 +1,63 @@ import { array, assign, boolean, enums, object, optional } from "superstruct"; import { LovelaceCardConfig } from "../../ha"; -import { ActionsSharedConfig, actionsSharedConfigStruct } from "../../shared/config/actions-config"; import { - AppearanceSharedConfig, - appearanceSharedConfigStruct, + ActionsSharedConfig, + actionsSharedConfigStruct, +} from "../../shared/config/actions-config"; +import { + AppearanceSharedConfig, + appearanceSharedConfigStruct, } from "../../shared/config/appearance-config"; -import { EntitySharedConfig, entitySharedConfigStruct } from "../../shared/config/entity-config"; +import { + EntitySharedConfig, + entitySharedConfigStruct, +} from "../../shared/config/entity-config"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; export const MEDIA_LAYER_MEDIA_CONTROLS = [ - "on_off", - "shuffle", - "previous", - "play_pause_stop", - "next", - "repeat", + "on_off", + "shuffle", + "previous", + "play_pause_stop", + "next", + "repeat", ] as const; -export type MediaPlayerMediaControl = (typeof MEDIA_LAYER_MEDIA_CONTROLS)[number]; +export type MediaPlayerMediaControl = + (typeof MEDIA_LAYER_MEDIA_CONTROLS)[number]; export const MEDIA_PLAYER_VOLUME_CONTROLS = [ - "volume_mute", - "volume_set", - "volume_buttons", + "volume_mute", + "volume_set", + "volume_buttons", ] as const; -export type MediaPlayerVolumeControl = (typeof MEDIA_PLAYER_VOLUME_CONTROLS)[number]; +export type MediaPlayerVolumeControl = + (typeof MEDIA_PLAYER_VOLUME_CONTROLS)[number]; export type MediaPlayerCardConfig = LovelaceCardConfig & - EntitySharedConfig & - AppearanceSharedConfig & - ActionsSharedConfig & { - use_media_info?: boolean; - show_volume_level?: boolean; - volume_controls?: MediaPlayerVolumeControl[]; - media_controls?: MediaPlayerMediaControl[]; - collapsible_controls?: boolean; - }; + EntitySharedConfig & + AppearanceSharedConfig & + ActionsSharedConfig & { + use_media_info?: boolean; + show_volume_level?: boolean; + volume_controls?: MediaPlayerVolumeControl[]; + media_controls?: MediaPlayerMediaControl[]; + collapsible_controls?: boolean; + }; export const mediaPlayerCardConfigStruct = assign( - lovelaceCardConfigStruct, - assign(entitySharedConfigStruct, appearanceSharedConfigStruct, actionsSharedConfigStruct), - object({ - use_media_info: optional(boolean()), - show_volume_level: optional(boolean()), - volume_controls: optional(array(enums(MEDIA_PLAYER_VOLUME_CONTROLS))), - media_controls: optional(array(enums(MEDIA_LAYER_MEDIA_CONTROLS))), - collapsible_controls: optional(boolean()), - }) + lovelaceCardConfigStruct, + assign( + entitySharedConfigStruct, + appearanceSharedConfigStruct, + actionsSharedConfigStruct + ), + object({ + use_media_info: optional(boolean()), + show_volume_level: optional(boolean()), + volume_controls: optional(array(enums(MEDIA_PLAYER_VOLUME_CONTROLS))), + media_controls: optional(array(enums(MEDIA_LAYER_MEDIA_CONTROLS))), + collapsible_controls: optional(boolean()), + }) ); diff --git a/src/cards/media-player-card/media-player-card-editor.ts b/src/cards/media-player-card/media-player-card-editor.ts index 82e55e9a7..494d935a3 100644 --- a/src/cards/media-player-card/media-player-card-editor.ts +++ b/src/cards/media-player-card/media-player-card-editor.ts @@ -10,121 +10,132 @@ import { MushroomBaseElement } from "../../utils/base-element"; import { GENERIC_LABELS } from "../../utils/form/generic-fields"; import { HaFormSchema } from "../../utils/form/ha-form"; import { loadHaComponents } from "../../utils/loader"; -import { MEDIA_PLAYER_CARD_EDITOR_NAME, MEDIA_PLAYER_ENTITY_DOMAINS } from "./const"; import { - MEDIA_LAYER_MEDIA_CONTROLS, - MEDIA_PLAYER_VOLUME_CONTROLS, - MediaPlayerCardConfig, - mediaPlayerCardConfigStruct, + MEDIA_PLAYER_CARD_EDITOR_NAME, + MEDIA_PLAYER_ENTITY_DOMAINS, +} from "./const"; +import { + MEDIA_LAYER_MEDIA_CONTROLS, + MEDIA_PLAYER_VOLUME_CONTROLS, + MediaPlayerCardConfig, + mediaPlayerCardConfigStruct, } from "./media-player-card-config"; export const MEDIA_LABELS = [ - "use_media_info", - "use_media_artwork", - "show_volume_level", - "media_controls", - "volume_controls", + "use_media_info", + "use_media_artwork", + "show_volume_level", + "media_controls", + "volume_controls", ]; const computeSchema = memoizeOne((localize: LocalizeFunc): HaFormSchema[] => [ - { name: "entity", selector: { entity: { domain: MEDIA_PLAYER_ENTITY_DOMAINS } } }, - { name: "name", selector: { text: {} } }, - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - ...APPEARANCE_FORM_SCHEMA, - { - type: "grid", - name: "", - schema: [ - { name: "use_media_info", selector: { boolean: {} } }, - { name: "show_volume_level", selector: { boolean: {} } }, - ], - }, - { - type: "grid", - name: "", - schema: [ - { - name: "volume_controls", - selector: { - select: { - options: MEDIA_PLAYER_VOLUME_CONTROLS.map((control) => ({ - value: control, - label: localize( - `editor.card.media-player.volume_controls_list.${control}` - ), - })), - mode: "list", - multiple: true, - }, - }, - }, - { - name: "media_controls", - selector: { - select: { - options: MEDIA_LAYER_MEDIA_CONTROLS.map((control) => ({ - value: control, - label: localize( - `editor.card.media-player.media_controls_list.${control}` - ), - })), - mode: "list", - multiple: true, - }, - }, - }, - { name: "collapsible_controls", selector: { boolean: {} } }, - ], - }, - ...computeActionsFormSchema(), + { + name: "entity", + selector: { entity: { domain: MEDIA_PLAYER_ENTITY_DOMAINS } }, + }, + { name: "name", selector: { text: {} } }, + { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, + ...APPEARANCE_FORM_SCHEMA, + { + type: "grid", + name: "", + schema: [ + { name: "use_media_info", selector: { boolean: {} } }, + { name: "show_volume_level", selector: { boolean: {} } }, + ], + }, + { + type: "grid", + name: "", + schema: [ + { + name: "volume_controls", + selector: { + select: { + options: MEDIA_PLAYER_VOLUME_CONTROLS.map((control) => ({ + value: control, + label: localize( + `editor.card.media-player.volume_controls_list.${control}` + ), + })), + mode: "list", + multiple: true, + }, + }, + }, + { + name: "media_controls", + selector: { + select: { + options: MEDIA_LAYER_MEDIA_CONTROLS.map((control) => ({ + value: control, + label: localize( + `editor.card.media-player.media_controls_list.${control}` + ), + })), + mode: "list", + multiple: true, + }, + }, + }, + { name: "collapsible_controls", selector: { boolean: {} } }, + ], + }, + ...computeActionsFormSchema(), ]); @customElement(MEDIA_PLAYER_CARD_EDITOR_NAME) -export class MediaCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: MediaPlayerCardConfig; +export class MediaCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: MediaPlayerCardConfig; - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } - public setConfig(config: MediaPlayerCardConfig): void { - assert(config, mediaPlayerCardConfigStruct); - this._config = config; - } + public setConfig(config: MediaPlayerCardConfig): void { + assert(config, mediaPlayerCardConfigStruct); + this._config = config; + } - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - if (MEDIA_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.media-player.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); + } + if (MEDIA_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.media-player.${schema.name}`); + } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + protected render() { + if (!this.hass || !this._config) { + return nothing; + } - const customLocalize = setupCustomlocalize(this.hass!); - const schema = computeSchema(customLocalize); + const customLocalize = setupCustomlocalize(this.hass!); + const schema = computeSchema(customLocalize); - return html` - - `; - } + return html` + + `; + } - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); - } + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/media-player-card/media-player-card.ts b/src/cards/media-player-card/media-player-card.ts index ae0e0ea77..4c14ba10c 100644 --- a/src/cards/media-player-card/media-player-card.ts +++ b/src/cards/media-player-card/media-player-card.ts @@ -1,18 +1,25 @@ -import { css, CSSResultGroup, html, nothing, PropertyValues, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + nothing, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { - actionHandler, - ActionHandlerEvent, - blankBeforePercent, - computeRTL, - handleAction, - hasAction, - HomeAssistant, - isActive, - LovelaceCard, - LovelaceCardEditor, - MediaPlayerEntity, + actionHandler, + ActionHandlerEvent, + blankBeforePercent, + computeRTL, + handleAction, + hasAction, + HomeAssistant, + isActive, + LovelaceCard, + LovelaceCardEditor, + MediaPlayerEntity, } from "../../ha"; import "../../shared/badge-icon"; import "../../shared/card"; @@ -25,9 +32,9 @@ import { registerCustomCard } from "../../utils/custom-cards"; import { computeEntityPicture } from "../../utils/info"; import { Layout } from "../../utils/layout"; import { - MEDIA_PLAYER_CARD_EDITOR_NAME, - MEDIA_PLAYER_CARD_NAME, - MEDIA_PLAYER_ENTITY_DOMAINS, + MEDIA_PLAYER_CARD_EDITOR_NAME, + MEDIA_PLAYER_CARD_NAME, + MEDIA_PLAYER_ENTITY_DOMAINS, } from "./const"; import "./controls/media-player-media-control"; import { isMediaControlVisible } from "./controls/media-player-media-control"; @@ -35,239 +42,259 @@ import "./controls/media-player-volume-control"; import { isVolumeControlVisible } from "./controls/media-player-volume-control"; import { MediaPlayerCardConfig } from "./media-player-card-config"; import { - computeMediaIcon, - computeMediaNameDisplay, - computeMediaStateDisplay, - getVolumeLevel, + computeMediaIcon, + computeMediaNameDisplay, + computeMediaStateDisplay, + getVolumeLevel, } from "./utils"; type MediaPlayerCardControl = "media_control" | "volume_control"; const CONTROLS_ICONS: Record = { - media_control: "mdi:play-pause", - volume_control: "mdi:volume-high", + media_control: "mdi:play-pause", + volume_control: "mdi:volume-high", }; registerCustomCard({ - type: MEDIA_PLAYER_CARD_NAME, - name: "Mushroom Media Card", - description: "Card for media player entity", + type: MEDIA_PLAYER_CARD_NAME, + name: "Mushroom Media Card", + description: "Card for media player entity", }); @customElement(MEDIA_PLAYER_CARD_NAME) export class MediaPlayerCard - extends MushroomBaseCard - implements LovelaceCard + extends MushroomBaseCard + implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./media-player-card-editor"); - return document.createElement(MEDIA_PLAYER_CARD_EDITOR_NAME) as LovelaceCardEditor; + public static async getConfigElement(): Promise { + await import("./media-player-card-editor"); + return document.createElement( + MEDIA_PLAYER_CARD_EDITOR_NAME + ) as LovelaceCardEditor; + } + + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + const mediaPlayers = entities.filter((e) => + MEDIA_PLAYER_ENTITY_DOMAINS.includes(e.split(".")[0]) + ); + return { + type: `custom:${MEDIA_PLAYER_CARD_NAME}`, + entity: mediaPlayers[0], + }; + } + + @state() private _activeControl?: MediaPlayerCardControl; + + protected get hasControls(): boolean { + return ( + Boolean(this._config?.media_controls?.length) || + Boolean(this._config?.volume_controls?.length) + ); + } + + private get _controls(): MediaPlayerCardControl[] { + if (!this._config || !this._stateObj) return []; + + const stateObj = this._stateObj; + + const controls: MediaPlayerCardControl[] = []; + if (isMediaControlVisible(stateObj, this._config.media_controls)) { + controls.push("media_control"); } - - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - const mediaPlayers = entities.filter((e) => - MEDIA_PLAYER_ENTITY_DOMAINS.includes(e.split(".")[0]) - ); - return { - type: `custom:${MEDIA_PLAYER_CARD_NAME}`, - entity: mediaPlayers[0], - }; + if (isVolumeControlVisible(stateObj, this._config.volume_controls)) { + controls.push("volume_control"); } - - @state() private _activeControl?: MediaPlayerCardControl; - - protected get hasControls(): boolean { - return ( - Boolean(this._config?.media_controls?.length) || - Boolean(this._config?.volume_controls?.length) - ); + return controls; + } + + _onControlTap(ctrl, e): void { + e.stopPropagation(); + this._activeControl = ctrl; + } + + setConfig(config: MediaPlayerCardConfig): void { + super.setConfig(config); + this.updateActiveControl(); + this.updateVolume(); + } + + protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if (this.hass && changedProperties.has("hass")) { + this.updateActiveControl(); + this.updateVolume(); } + } - private get _controls(): MediaPlayerCardControl[] { - if (!this._config || !this._stateObj) return []; + @state() + private volume?: number; - const stateObj = this._stateObj; + updateVolume() { + this.volume = undefined; + const stateObj = this._stateObj; - const controls: MediaPlayerCardControl[] = []; - if (isMediaControlVisible(stateObj, this._config.media_controls)) { - controls.push("media_control"); - } - if (isVolumeControlVisible(stateObj, this._config.volume_controls)) { - controls.push("volume_control"); - } - return controls; - } + if (!stateObj) return; + const volume = getVolumeLevel(stateObj); + this.volume = volume != null ? Math.round(volume) : volume; + } - _onControlTap(ctrl, e): void { - e.stopPropagation(); - this._activeControl = ctrl; + private onCurrentVolumeChange(e: CustomEvent<{ value?: number }>): void { + if (e.detail.value != null) { + this.volume = e.detail.value; } - - setConfig(config: MediaPlayerCardConfig): void { - super.setConfig(config); - this.updateActiveControl(); - this.updateVolume(); + } + + updateActiveControl() { + const isActiveControlSupported = this._activeControl + ? this._controls.includes(this._activeControl) + : false; + this._activeControl = isActiveControlSupported + ? this._activeControl + : this._controls[0]; + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + protected render() { + if (!this._config || !this.hass || !this._config.entity) { + return nothing; } - protected updated(changedProperties: PropertyValues) { - super.updated(changedProperties); - if (this.hass && changedProperties.has("hass")) { - this.updateActiveControl(); - this.updateVolume(); - } - } - - @state() - private volume?: number; - - updateVolume() { - this.volume = undefined; - const stateObj = this._stateObj; - - if (!stateObj) return; - const volume = getVolumeLevel(stateObj); - this.volume = volume != null ? Math.round(volume) : volume; - } - - private onCurrentVolumeChange(e: CustomEvent<{ value?: number }>): void { - if (e.detail.value != null) { - this.volume = e.detail.value; - } - } - - updateActiveControl() { - const isActiveControlSupported = this._activeControl - ? this._controls.includes(this._activeControl) - : false; - this._activeControl = isActiveControlSupported ? this._activeControl : this._controls[0]; - } + const stateObj = this._stateObj; - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); + if (!stateObj) { + return this.renderNotFound(this._config); } - protected render() { - if (!this._config || !this.hass || !this._config.entity) { - return nothing; - } - - const stateObj = this._stateObj; - - if (!stateObj) { - return this.renderNotFound(this._config); - } - - const icon = computeMediaIcon(this._config, stateObj); - const nameDisplay = computeMediaNameDisplay(this._config, stateObj); - const stateDisplay = computeMediaStateDisplay(this._config, stateObj, this.hass); - const appearance = computeAppearance(this._config); - const picture = computeEntityPicture(stateObj, appearance.icon_type); - - const stateValue = - this.volume != null && this._config.show_volume_level - ? `${stateDisplay} - ${this.volume}${blankBeforePercent(this.hass.locale)}%` - : stateDisplay; - - const rtl = computeRTL(this.hass); - - const isControlVisible = - (!this._config.collapsible_controls || isActive(stateObj)) && this._controls.length; - + const icon = computeMediaIcon(this._config, stateObj); + const nameDisplay = computeMediaNameDisplay(this._config, stateObj); + const stateDisplay = computeMediaStateDisplay( + this._config, + stateObj, + this.hass + ); + const appearance = computeAppearance(this._config); + const picture = computeEntityPicture(stateObj, appearance.icon_type); + + const stateValue = + this.volume != null && this._config.show_volume_level + ? `${stateDisplay} - ${this.volume}${blankBeforePercent(this.hass.locale)}%` + : stateDisplay; + + const rtl = computeRTL(this.hass); + + const isControlVisible = + (!this._config.collapsible_controls || isActive(stateObj)) && + this._controls.length; + + return html` + + + + ${picture + ? this.renderPicture(picture) + : this.renderIcon(stateObj, icon)} + ${this.renderBadge(stateObj)} + ${this.renderStateInfo( + stateObj, + appearance, + nameDisplay, + stateValue + )}; + + ${isControlVisible + ? html` +
+ ${this.renderActiveControl(stateObj, appearance.layout)} + ${this.renderOtherControls()} +
+ ` + : nothing} +
+
+ `; + } + + private renderOtherControls(): TemplateResult | null { + const otherControls = this._controls.filter( + (control) => control != this._activeControl + ); + + return html` + ${otherControls.map( + (ctrl) => html` + this._onControlTap(ctrl, e)}> + + + ` + )} + `; + } + + private renderActiveControl(entity: MediaPlayerEntity, layout: Layout) { + const media_controls = this._config?.media_controls ?? []; + const volume_controls = this._config?.volume_controls ?? []; + + switch (this._activeControl) { + case "media_control": return html` - - - - ${picture ? this.renderPicture(picture) : this.renderIcon(stateObj, icon)} - ${this.renderBadge(stateObj)} - ${this.renderStateInfo(stateObj, appearance, nameDisplay, stateValue)}; - - ${isControlVisible - ? html` -
- ${this.renderActiveControl(stateObj, appearance.layout)} - ${this.renderOtherControls()} -
- ` - : nothing} -
-
+ + `; - } - - private renderOtherControls(): TemplateResult | null { - const otherControls = this._controls.filter((control) => control != this._activeControl); - + case "volume_control": return html` - ${otherControls.map( - (ctrl) => html` - this._onControlTap(ctrl, e)}> - - - ` - )} + `; + default: + return nothing; } - - private renderActiveControl(entity: MediaPlayerEntity, layout: Layout) { - const media_controls = this._config?.media_controls ?? []; - const volume_controls = this._config?.volume_controls ?? []; - - switch (this._activeControl) { - case "media_control": - return html` - - - `; - case "volume_control": - return html` - - `; - default: - return nothing; + } + + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + mushroom-state-item { + cursor: pointer; } - } - - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - mushroom-state-item { - cursor: pointer; - } - mushroom-shape-icon { - --icon-color: rgb(var(--rgb-state-media-player)); - --shape-color: rgba(var(--rgb-state-media-player), 0.2); - } - mushroom-media-player-media-control, - mushroom-media-player-volume-control { - flex: 1; - } - `, - ]; - } + mushroom-shape-icon { + --icon-color: rgb(var(--rgb-state-media-player)); + --shape-color: rgba(var(--rgb-state-media-player), 0.2); + } + mushroom-media-player-media-control, + mushroom-media-player-volume-control { + flex: 1; + } + `, + ]; + } } diff --git a/src/cards/media-player-card/utils.ts b/src/cards/media-player-card/utils.ts index 563f0329d..58359d268 100644 --- a/src/cards/media-player-card/utils.ts +++ b/src/cards/media-player-card/utils.ts @@ -1,272 +1,295 @@ import { HassEntity } from "home-assistant-js-websocket"; import { - HomeAssistant, - MEDIA_PLAYER_SUPPORT_NEXT_TRACK, - MEDIA_PLAYER_SUPPORT_PAUSE, - MEDIA_PLAYER_SUPPORT_PLAY, - MEDIA_PLAYER_SUPPORT_PREVIOUS_TRACK, - MEDIA_PLAYER_SUPPORT_REPEAT_SET, - MEDIA_PLAYER_SUPPORT_SHUFFLE_SET, - MEDIA_PLAYER_SUPPORT_STOP, - MEDIA_PLAYER_SUPPORT_TURN_OFF, - MEDIA_PLAYER_SUPPORT_TURN_ON, - MediaPlayerEntity, - OFF, - UNAVAILABLE, - UNKNOWN, - computeMediaDescription, - computeStateDisplay, - supportsFeature, + HomeAssistant, + MEDIA_PLAYER_SUPPORT_NEXT_TRACK, + MEDIA_PLAYER_SUPPORT_PAUSE, + MEDIA_PLAYER_SUPPORT_PLAY, + MEDIA_PLAYER_SUPPORT_PREVIOUS_TRACK, + MEDIA_PLAYER_SUPPORT_REPEAT_SET, + MEDIA_PLAYER_SUPPORT_SHUFFLE_SET, + MEDIA_PLAYER_SUPPORT_STOP, + MEDIA_PLAYER_SUPPORT_TURN_OFF, + MEDIA_PLAYER_SUPPORT_TURN_ON, + MediaPlayerEntity, + OFF, + UNAVAILABLE, + UNKNOWN, + computeMediaDescription, + computeStateDisplay, + supportsFeature, } from "../../ha"; -import { MediaPlayerCardConfig, MediaPlayerMediaControl } from "./media-player-card-config"; +import { + MediaPlayerCardConfig, + MediaPlayerMediaControl, +} from "./media-player-card-config"; export function callService( - e: MouseEvent, - hass: HomeAssistant, - stateObj: HassEntity, - serviceName: string + e: MouseEvent, + hass: HomeAssistant, + stateObj: HassEntity, + serviceName: string ): void { - e.stopPropagation(); - hass.callService("media_player", serviceName, { - entity_id: stateObj.entity_id, - }); + e.stopPropagation(); + hass.callService("media_player", serviceName, { + entity_id: stateObj.entity_id, + }); } export function computeMediaNameDisplay( - config: MediaPlayerCardConfig, - entity: MediaPlayerEntity + config: MediaPlayerCardConfig, + entity: MediaPlayerEntity ): string { - let name = config.name || entity.attributes.friendly_name || ""; - if (![UNAVAILABLE, UNKNOWN, OFF].includes(entity.state) && config.use_media_info) { - if (entity.attributes.media_title) { - name = entity.attributes.media_title; - } + let name = config.name || entity.attributes.friendly_name || ""; + if ( + ![UNAVAILABLE, UNKNOWN, OFF].includes(entity.state) && + config.use_media_info + ) { + if (entity.attributes.media_title) { + name = entity.attributes.media_title; } - return name; + } + return name; } export function computeMediaStateDisplay( - config: MediaPlayerCardConfig, - entity: MediaPlayerEntity, - hass: HomeAssistant + config: MediaPlayerCardConfig, + entity: MediaPlayerEntity, + hass: HomeAssistant ): string { - let state = hass.formatEntityState - ? hass.formatEntityState(entity) - : computeStateDisplay(hass.localize, entity, hass.locale, hass.config, hass.entities); - if (![UNAVAILABLE, UNKNOWN, OFF].includes(entity.state) && config.use_media_info) { - return computeMediaDescription(entity) || state; - } - return state; + let state = hass.formatEntityState + ? hass.formatEntityState(entity) + : computeStateDisplay( + hass.localize, + entity, + hass.locale, + hass.config, + hass.entities + ); + if ( + ![UNAVAILABLE, UNKNOWN, OFF].includes(entity.state) && + config.use_media_info + ) { + return computeMediaDescription(entity) || state; + } + return state; } export function getVolumeLevel(entity: MediaPlayerEntity) { - return entity.attributes.volume_level != null - ? entity.attributes.volume_level * 100 - : undefined; + return entity.attributes.volume_level != null + ? entity.attributes.volume_level * 100 + : undefined; } export function computeMediaIcon( - config: MediaPlayerCardConfig, - entity: MediaPlayerEntity + config: MediaPlayerCardConfig, + entity: MediaPlayerEntity ): string | undefined { - var icon = config.icon; + var icon = config.icon; - if (![UNAVAILABLE, UNKNOWN, OFF].includes(entity.state) && config.use_media_info) { - var app = entity.attributes.app_name?.toLowerCase(); - switch (app) { - case "spotify": - return "mdi:spotify"; - case "google podcasts": - return "mdi:google-podcast"; - case "plex": - return "mdi:plex"; - case "soundcloud": - return "mdi:soundcloud"; - case "youtube": - return "mdi:youtube"; - case "oto music": - return "mdi:music-circle"; - case "netflix": - return "mdi:netflix"; - default: - return undefined; - } + if ( + ![UNAVAILABLE, UNKNOWN, OFF].includes(entity.state) && + config.use_media_info + ) { + var app = entity.attributes.app_name?.toLowerCase(); + switch (app) { + case "spotify": + return "mdi:spotify"; + case "google podcasts": + return "mdi:google-podcast"; + case "plex": + return "mdi:plex"; + case "soundcloud": + return "mdi:soundcloud"; + case "youtube": + return "mdi:youtube"; + case "oto music": + return "mdi:music-circle"; + case "netflix": + return "mdi:netflix"; + default: + return undefined; } - return icon; + } + return icon; } export interface ControlButton { - icon: string; - action: string; + icon: string; + action: string; } export const computeMediaControls = ( - stateObj: MediaPlayerEntity, - controls: MediaPlayerMediaControl[] + stateObj: MediaPlayerEntity, + controls: MediaPlayerMediaControl[] ): ControlButton[] => { - if (!stateObj) { - return []; - } + if (!stateObj) { + return []; + } - const state = stateObj.state; + const state = stateObj.state; - if (state === "off") { - return supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_TURN_ON) && - controls.includes("on_off") - ? [ - { - icon: "mdi:power", - action: "turn_on", - }, - ] - : []; - } + if (state === "off") { + return supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_TURN_ON) && + controls.includes("on_off") + ? [ + { + icon: "mdi:power", + action: "turn_on", + }, + ] + : []; + } - const buttons: ControlButton[] = []; + const buttons: ControlButton[] = []; - if (supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_TURN_OFF) && controls.includes("on_off")) { - buttons.push({ - icon: "mdi:power", - action: "turn_off", - }); - } + if ( + supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_TURN_OFF) && + controls.includes("on_off") + ) { + buttons.push({ + icon: "mdi:power", + action: "turn_off", + }); + } - const assumedState = stateObj.attributes.assumed_state === true; - const stateAttr = stateObj.attributes; + const assumedState = stateObj.attributes.assumed_state === true; + const stateAttr = stateObj.attributes; - if ( - (state === "playing" || state === "paused" || assumedState) && - supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_SHUFFLE_SET) && - controls.includes("shuffle") - ) { - buttons.push({ - icon: stateAttr.shuffle === true ? "mdi:shuffle" : "mdi:shuffle-disabled", - action: "shuffle_set", - }); - } + if ( + (state === "playing" || state === "paused" || assumedState) && + supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_SHUFFLE_SET) && + controls.includes("shuffle") + ) { + buttons.push({ + icon: stateAttr.shuffle === true ? "mdi:shuffle" : "mdi:shuffle-disabled", + action: "shuffle_set", + }); + } - if ( - (state === "playing" || state === "paused" || assumedState) && - supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PREVIOUS_TRACK) && - controls.includes("previous") - ) { - buttons.push({ - icon: "mdi:skip-previous", - action: "media_previous_track", - }); - } + if ( + (state === "playing" || state === "paused" || assumedState) && + supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PREVIOUS_TRACK) && + controls.includes("previous") + ) { + buttons.push({ + icon: "mdi:skip-previous", + action: "media_previous_track", + }); + } - if ( - !assumedState && - ((state === "playing" && - (supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PAUSE) || - supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_STOP))) || - ((state === "paused" || state === "idle") && - supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PLAY)) || - (state === "on" && - (supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PLAY) || - supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PAUSE)))) && - controls.includes("play_pause_stop") - ) { - buttons.push({ - icon: - state === "on" - ? "mdi:play-pause" - : state !== "playing" - ? "mdi:play" - : supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PAUSE) - ? "mdi:pause" - : "mdi:stop", - action: - state !== "playing" - ? "media_play" - : supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PAUSE) - ? "media_pause" - : "media_stop", - }); - } + if ( + !assumedState && + ((state === "playing" && + (supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PAUSE) || + supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_STOP))) || + ((state === "paused" || state === "idle") && + supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PLAY)) || + (state === "on" && + (supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PLAY) || + supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PAUSE)))) && + controls.includes("play_pause_stop") + ) { + buttons.push({ + icon: + state === "on" + ? "mdi:play-pause" + : state !== "playing" + ? "mdi:play" + : supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PAUSE) + ? "mdi:pause" + : "mdi:stop", + action: + state !== "playing" + ? "media_play" + : supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PAUSE) + ? "media_pause" + : "media_stop", + }); + } - if ( - assumedState && - supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PLAY) && - controls.includes("play_pause_stop") - ) { - buttons.push({ - icon: "mdi:play", - action: "media_play", - }); - } + if ( + assumedState && + supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PLAY) && + controls.includes("play_pause_stop") + ) { + buttons.push({ + icon: "mdi:play", + action: "media_play", + }); + } - if ( - assumedState && - supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PAUSE) && - controls.includes("play_pause_stop") - ) { - buttons.push({ - icon: "mdi:pause", - action: "media_pause", - }); - } + if ( + assumedState && + supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_PAUSE) && + controls.includes("play_pause_stop") + ) { + buttons.push({ + icon: "mdi:pause", + action: "media_pause", + }); + } - if ( - assumedState && - supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_STOP) && - controls.includes("play_pause_stop") - ) { - buttons.push({ - icon: "mdi:stop", - action: "media_stop", - }); - } + if ( + assumedState && + supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_STOP) && + controls.includes("play_pause_stop") + ) { + buttons.push({ + icon: "mdi:stop", + action: "media_stop", + }); + } - if ( - (state === "playing" || state === "paused" || assumedState) && - supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_NEXT_TRACK) && - controls.includes("next") - ) { - buttons.push({ - icon: "mdi:skip-next", - action: "media_next_track", - }); - } + if ( + (state === "playing" || state === "paused" || assumedState) && + supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_NEXT_TRACK) && + controls.includes("next") + ) { + buttons.push({ + icon: "mdi:skip-next", + action: "media_next_track", + }); + } - if ( - (state === "playing" || state === "paused" || assumedState) && - supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_REPEAT_SET) && - controls.includes("repeat") - ) { - buttons.push({ - icon: - stateAttr.repeat === "all" - ? "mdi:repeat" - : stateAttr.repeat === "one" - ? "mdi:repeat-once" - : "mdi:repeat-off", - action: "repeat_set", - }); - } + if ( + (state === "playing" || state === "paused" || assumedState) && + supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_REPEAT_SET) && + controls.includes("repeat") + ) { + buttons.push({ + icon: + stateAttr.repeat === "all" + ? "mdi:repeat" + : stateAttr.repeat === "one" + ? "mdi:repeat-once" + : "mdi:repeat-off", + action: "repeat_set", + }); + } - return buttons.length > 0 ? buttons : []; + return buttons.length > 0 ? buttons : []; }; export const formatMediaTime = (seconds: number | undefined): string => { - if (seconds === undefined || seconds === Infinity) { - return ""; - } + if (seconds === undefined || seconds === Infinity) { + return ""; + } - let secondsString = new Date(seconds * 1000).toISOString(); - secondsString = - seconds > 3600 ? secondsString.substring(11, 16) : secondsString.substring(14, 19); - return secondsString.replace(/^0+/, "").padStart(4, "0"); + let secondsString = new Date(seconds * 1000).toISOString(); + secondsString = + seconds > 3600 + ? secondsString.substring(11, 16) + : secondsString.substring(14, 19); + return secondsString.replace(/^0+/, "").padStart(4, "0"); }; export const cleanupMediaTitle = (title?: string): string | undefined => { - if (!title) { - return undefined; - } + if (!title) { + return undefined; + } - const index = title.indexOf("?authSig="); - return index > 0 ? title.slice(0, index) : title; + const index = title.indexOf("?authSig="); + return index > 0 ? title.slice(0, index) : title; }; /** @@ -277,39 +300,40 @@ export const cleanupMediaTitle = (title?: string): string | undefined => { * @returns */ export const setMediaPlayerVolume = ( - hass: HomeAssistant, - entity_id: string, - volume_level: number -) => hass.callService("media_player", "volume_set", { entity_id, volume_level }); + hass: HomeAssistant, + entity_id: string, + volume_level: number +) => + hass.callService("media_player", "volume_set", { entity_id, volume_level }); export const handleMediaControlClick = ( - hass: HomeAssistant, - stateObj: MediaPlayerEntity, - action: string + hass: HomeAssistant, + stateObj: MediaPlayerEntity, + action: string ) => { - let parameters = {}; + let parameters = {}; - if (action === "shuffle_set") { - parameters = { - shuffle: !stateObj!.attributes.shuffle, - }; - } else if (action === "repeat_set") { - parameters = { - repeat: - stateObj!.attributes.repeat === "all" - ? "one" - : stateObj!.attributes.repeat === "off" - ? "all" - : "off", - }; - } else if (action === "volume_mute") { - parameters = { - is_volume_muted: !stateObj!.attributes.is_volume_muted, - }; - } + if (action === "shuffle_set") { + parameters = { + shuffle: !stateObj!.attributes.shuffle, + }; + } else if (action === "repeat_set") { + parameters = { + repeat: + stateObj!.attributes.repeat === "all" + ? "one" + : stateObj!.attributes.repeat === "off" + ? "all" + : "off", + }; + } else if (action === "volume_mute") { + parameters = { + is_volume_muted: !stateObj!.attributes.is_volume_muted, + }; + } - hass.callService("media_player", action, { - entity_id: stateObj!.entity_id, - ...parameters, - }); + hass.callService("media_player", action, { + entity_id: stateObj!.entity_id, + ...parameters, + }); }; diff --git a/src/cards/number-card/controls/number-value-control.ts b/src/cards/number-card/controls/number-value-control.ts index 2ec84b7a8..4e0f45cd7 100644 --- a/src/cards/number-card/controls/number-value-control.ts +++ b/src/cards/number-card/controls/number-value-control.ts @@ -2,93 +2,95 @@ import { HassEntity } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; import { - getDefaultFormatOptions, - getNumberFormatOptions, - HomeAssistant, - isActive, - isAvailable, + getDefaultFormatOptions, + getNumberFormatOptions, + HomeAssistant, + isActive, + isAvailable, } from "../../../ha"; import "../../../shared/slider"; import "../../../shared/input-number"; @customElement("mushroom-number-value-control") export class NumberValueControl extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public entity!: HassEntity; + @property({ attribute: false }) public entity!: HassEntity; - @property({ attribute: false }) public displayMode?: "slider" | "buttons"; + @property({ attribute: false }) public displayMode?: "slider" | "buttons"; - onChange(e: CustomEvent<{ value: number }>): void { - const value = e.detail.value; - const domain = this.entity.entity_id.split(".")[0]; - this.hass.callService(domain, "set_value", { - entity_id: this.entity.entity_id, - value: value, - }); - } - - onCurrentChange(e: CustomEvent<{ value?: number }>): void { - const value = e.detail.value; + onChange(e: CustomEvent<{ value: number }>): void { + const value = e.detail.value; + const domain = this.entity.entity_id.split(".")[0]; + this.hass.callService(domain, "set_value", { + entity_id: this.entity.entity_id, + value: value, + }); + } - this.dispatchEvent( - new CustomEvent("current-change", { - detail: { - value, - }, - }) - ); - } + onCurrentChange(e: CustomEvent<{ value?: number }>): void { + const value = e.detail.value; - protected render(): TemplateResult { - const value = Number(this.entity.state); + this.dispatchEvent( + new CustomEvent("current-change", { + detail: { + value, + }, + }) + ); + } - const formatOptions = - getNumberFormatOptions(this.entity, this.hass.entities[this.entity.entity_id]) ?? - getDefaultFormatOptions(this.entity.state); + protected render(): TemplateResult { + const value = Number(this.entity.state); - if (this.displayMode === "buttons") { - return html` - - `; - } + const formatOptions = + getNumberFormatOptions( + this.entity, + this.hass.entities[this.entity.entity_id] + ) ?? getDefaultFormatOptions(this.entity.state); - return html` - - `; + if (this.displayMode === "buttons") { + return html` + + `; } - static get styles(): CSSResultGroup { - return css` - :host { - --slider-color: rgb(var(--rgb-state-number)); - --slider-outline-color: transparent; - --slider-bg-color: rgba(var(--rgb-state-number), 0.2); - } - mushroom-slider { - --main-color: var(--slider-color); - --bg-color: var(--slider-bg-color); - --main-outline-color: var(--slider-outline-color); - } - `; - } + return html` + + `; + } + + static get styles(): CSSResultGroup { + return css` + :host { + --slider-color: rgb(var(--rgb-state-number)); + --slider-outline-color: transparent; + --slider-bg-color: rgba(var(--rgb-state-number), 0.2); + } + mushroom-slider { + --main-color: var(--slider-color); + --bg-color: var(--slider-bg-color); + --main-outline-color: var(--slider-outline-color); + } + `; + } } diff --git a/src/cards/number-card/number-card-config.ts b/src/cards/number-card/number-card-config.ts index 49b5b45ac..d305aff6b 100644 --- a/src/cards/number-card/number-card-config.ts +++ b/src/cards/number-card/number-card-config.ts @@ -1,11 +1,25 @@ -import { assign, enums, literal, object, optional, string, union } from "superstruct"; +import { + assign, + enums, + literal, + object, + optional, + string, + union, +} from "superstruct"; import { LovelaceCardConfig } from "../../ha"; -import { ActionsSharedConfig, actionsSharedConfigStruct } from "../../shared/config/actions-config"; import { - AppearanceSharedConfig, - appearanceSharedConfigStruct, + ActionsSharedConfig, + actionsSharedConfigStruct, +} from "../../shared/config/actions-config"; +import { + AppearanceSharedConfig, + appearanceSharedConfigStruct, } from "../../shared/config/appearance-config"; -import { EntitySharedConfig, entitySharedConfigStruct } from "../../shared/config/entity-config"; +import { + EntitySharedConfig, + entitySharedConfigStruct, +} from "../../shared/config/entity-config"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; export const DISPLAY_MODES = ["slider", "buttons"] as const; @@ -13,18 +27,22 @@ export const DISPLAY_MODES = ["slider", "buttons"] as const; type DisplayMode = (typeof DISPLAY_MODES)[number]; export type NumberCardConfig = LovelaceCardConfig & - EntitySharedConfig & - AppearanceSharedConfig & - ActionsSharedConfig & { - icon_color?: string; - display_mode?: DisplayMode; - }; + EntitySharedConfig & + AppearanceSharedConfig & + ActionsSharedConfig & { + icon_color?: string; + display_mode?: DisplayMode; + }; export const NumberCardConfigStruct = assign( - lovelaceCardConfigStruct, - assign(entitySharedConfigStruct, appearanceSharedConfigStruct, actionsSharedConfigStruct), - object({ - icon_color: optional(string()), - display_mode: optional(enums(DISPLAY_MODES)), - }) + lovelaceCardConfigStruct, + assign( + entitySharedConfigStruct, + appearanceSharedConfigStruct, + actionsSharedConfigStruct + ), + object({ + icon_color: optional(string()), + display_mode: optional(enums(DISPLAY_MODES)), + }) ); diff --git a/src/cards/number-card/number-card-editor.ts b/src/cards/number-card/number-card-editor.ts index a02f37be0..cd63cde35 100644 --- a/src/cards/number-card/number-card-editor.ts +++ b/src/cards/number-card/number-card-editor.ts @@ -11,97 +11,110 @@ import { GENERIC_LABELS } from "../../utils/form/generic-fields"; import { HaFormSchema } from "../../utils/form/ha-form"; import { loadHaComponents } from "../../utils/loader"; import { NUMBER_CARD_EDITOR_NAME, NUMBER_ENTITY_DOMAINS } from "./const"; -import { DISPLAY_MODES, NumberCardConfig, NumberCardConfigStruct } from "./number-card-config"; +import { + DISPLAY_MODES, + NumberCardConfig, + NumberCardConfigStruct, +} from "./number-card-config"; export const NUMBER_LABELS = ["display_mode"]; const computeSchema = memoizeOne((localize: LocalizeFunc): HaFormSchema[] => [ - { name: "entity", selector: { entity: { domain: NUMBER_ENTITY_DOMAINS } } }, - { name: "name", selector: { text: {} } }, - { - type: "grid", - name: "", - schema: [ - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - { name: "icon_color", selector: { mush_color: {} } }, - ], + { name: "entity", selector: { entity: { domain: NUMBER_ENTITY_DOMAINS } } }, + { name: "name", selector: { text: {} } }, + { + type: "grid", + name: "", + schema: [ + { + name: "icon", + selector: { icon: {} }, + context: { icon_entity: "entity" }, + }, + { name: "icon_color", selector: { mush_color: {} } }, + ], + }, + ...APPEARANCE_FORM_SCHEMA, + { + name: "display_mode", + selector: { + select: { + options: ["default", ...DISPLAY_MODES].map((control) => ({ + value: control, + label: localize(`editor.card.number.display_mode_list.${control}`), + })), + mode: "dropdown", + }, }, - ...APPEARANCE_FORM_SCHEMA, - { - name: "display_mode", - selector: { - select: { - options: ["default", ...DISPLAY_MODES].map((control) => ({ - value: control, - label: localize(`editor.card.number.display_mode_list.${control}`), - })), - mode: "dropdown", - }, - }, - }, - ...computeActionsFormSchema(), + }, + ...computeActionsFormSchema(), ]); @customElement(NUMBER_CARD_EDITOR_NAME) -export class NumberCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: NumberCardConfig; - - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); +export class NumberCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: NumberCardConfig; + + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } + + public setConfig(config: NumberCardConfig): void { + assert(config, NumberCardConfigStruct); + this._config = config; + } + + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); + + if (NUMBER_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.number.${schema.name}`); } - public setConfig(config: NumberCardConfig): void { - assert(config, NumberCardConfigStruct); - this._config = config; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); } - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); - - if (NUMBER_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.number.${schema.name}`); - } - - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - protected render() { - if (!this.hass || !this._config) { - return nothing; - } - - const customLocalize = setupCustomlocalize(this.hass); + protected render() { + if (!this.hass || !this._config) { + return nothing; + } - const schema = computeSchema(customLocalize); + const customLocalize = setupCustomlocalize(this.hass); - const data = { ...this._config } as any; - if (!data.display_mode) { - data.display_mode = "default"; - } + const schema = computeSchema(customLocalize); - return html` - - `; + const data = { ...this._config } as any; + if (!data.display_mode) { + data.display_mode = "default"; } - private _valueChanged(ev: CustomEvent): void { - const config = { ...ev.detail.value }; - - if (config.display_mode === "default") { - delete config.display_mode; - } - - fireEvent(this, "config-changed", { config }); + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + const config = { ...ev.detail.value }; + + if (config.display_mode === "default") { + delete config.display_mode; } + + fireEvent(this, "config-changed", { config }); + } } diff --git a/src/cards/number-card/number-card.ts b/src/cards/number-card/number-card.ts index 3aa2b0ef9..5ef2e9d46 100644 --- a/src/cards/number-card/number-card.ts +++ b/src/cards/number-card/number-card.ts @@ -1,22 +1,29 @@ import { HassEntity } from "home-assistant-js-websocket"; -import { css, CSSResultGroup, html, nothing, PropertyValues, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + nothing, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - computeRTL, - computeStateDisplay, - formatNumber, - getDefaultFormatOptions, - getNumberFormatOptions, - handleAction, - hasAction, - HomeAssistant, - isActive, - LovelaceCard, - LovelaceCardEditor, + actionHandler, + ActionHandlerEvent, + computeRTL, + computeStateDisplay, + formatNumber, + getDefaultFormatOptions, + getNumberFormatOptions, + handleAction, + hasAction, + HomeAssistant, + isActive, + LovelaceCard, + LovelaceCardEditor, } from "../../ha"; import "../../shared/badge-icon"; import "../../shared/button"; @@ -31,174 +38,197 @@ import { cardStyle } from "../../utils/card-styles"; import { computeRgbColor } from "../../utils/colors"; import { registerCustomCard } from "../../utils/custom-cards"; import { computeEntityPicture } from "../../utils/info"; -import { NUMBER_CARD_EDITOR_NAME, NUMBER_CARD_NAME, NUMBER_ENTITY_DOMAINS } from "./const"; +import { + NUMBER_CARD_EDITOR_NAME, + NUMBER_CARD_NAME, + NUMBER_ENTITY_DOMAINS, +} from "./const"; import "./controls/number-value-control"; import { NumberCardConfig } from "./number-card-config"; registerCustomCard({ - type: NUMBER_CARD_NAME, - name: "Mushroom Number Card", - description: "Card for number and input number entity", + type: NUMBER_CARD_NAME, + name: "Mushroom Number Card", + description: "Card for number and input number entity", }); @customElement(NUMBER_CARD_NAME) -export class NumberCard extends MushroomBaseCard implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./number-card-editor"); - return document.createElement(NUMBER_CARD_EDITOR_NAME) as LovelaceCardEditor; +export class NumberCard + extends MushroomBaseCard + implements LovelaceCard +{ + public static async getConfigElement(): Promise { + await import("./number-card-editor"); + return document.createElement( + NUMBER_CARD_EDITOR_NAME + ) as LovelaceCardEditor; + } + + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + const numbers = entities.filter((e) => + NUMBER_ENTITY_DOMAINS.includes(e.split(".")[0]) + ); + return { + type: `custom:${NUMBER_CARD_NAME}`, + entity: numbers[0], + }; + } + + protected get hasControls(): boolean { + return true; + } + + @state() private value?: number; + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + private onCurrentValueChange(e: CustomEvent<{ value?: number }>): void { + if (e.detail.value != null) { + this.value = e.detail.value; } + } - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - const numbers = entities.filter((e) => NUMBER_ENTITY_DOMAINS.includes(e.split(".")[0])); - return { - type: `custom:${NUMBER_CARD_NAME}`, - entity: numbers[0], - }; + protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if (this.hass && changedProperties.has("hass")) { + this.updateValue(); } + } - protected get hasControls(): boolean { - return true; - } + updateValue() { + this.value = undefined; + const stateObj = this._stateObj; - @state() private value?: number; + if (!stateObj || Number.isNaN(stateObj.state)) return; + this.value = Number(stateObj.state); + } - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); + protected render() { + if (!this._config || !this.hass || !this._config.entity) { + return nothing; } - private onCurrentValueChange(e: CustomEvent<{ value?: number }>): void { - if (e.detail.value != null) { - this.value = e.detail.value; - } - } + const stateObj = this._stateObj; - protected updated(changedProperties: PropertyValues) { - super.updated(changedProperties); - if (this.hass && changedProperties.has("hass")) { - this.updateValue(); - } + if (!stateObj) { + return this.renderNotFound(this._config); } - updateValue() { - this.value = undefined; - const stateObj = this._stateObj; - - if (!stateObj || Number.isNaN(stateObj.state)) return; - this.value = Number(stateObj.state); + const name = this._config.name || stateObj.attributes.friendly_name || ""; + const icon = this._config.icon; + const appearance = computeAppearance(this._config); + const picture = computeEntityPicture(stateObj, appearance.icon_type); + + let stateDisplay = this.hass.formatEntityState + ? this.hass.formatEntityState(stateObj) + : computeStateDisplay( + this.hass.localize, + stateObj, + this.hass.locale, + this.hass.config, + this.hass.entities + ); + if (this.value !== undefined) { + const numberValue = formatNumber( + this.value, + this.hass.locale, + getNumberFormatOptions( + stateObj, + this.hass.entities[stateObj.entity_id] + ) ?? getDefaultFormatOptions(stateObj.state) + ); + stateDisplay = `${numberValue} ${stateObj.attributes.unit_of_measurement ?? ""}`; } - protected render() { - if (!this._config || !this.hass || !this._config.entity) { - return nothing; - } + const rtl = computeRTL(this.hass); - const stateObj = this._stateObj; - - if (!stateObj) { - return this.renderNotFound(this._config); - } + const sliderStyle = {}; + const iconColor = this._config?.icon_color; + if (iconColor) { + const iconRgbColor = computeRgbColor(iconColor); + sliderStyle["--slider-color"] = `rgb(${iconRgbColor})`; + sliderStyle["--slider-bg-color"] = `rgba(${iconRgbColor}, 0.2)`; + } - const name = this._config.name || stateObj.attributes.friendly_name || ""; - const icon = this._config.icon; - const appearance = computeAppearance(this._config); - const picture = computeEntityPicture(stateObj, appearance.icon_type); - - let stateDisplay = this.hass.formatEntityState - ? this.hass.formatEntityState(stateObj) - : computeStateDisplay( - this.hass.localize, - stateObj, - this.hass.locale, - this.hass.config, - this.hass.entities - ); - if (this.value !== undefined) { - const numberValue = formatNumber( - this.value, - this.hass.locale, - getNumberFormatOptions(stateObj, this.hass.entities[stateObj.entity_id]) ?? - getDefaultFormatOptions(stateObj.state) - ); - stateDisplay = `${numberValue} ${stateObj.attributes.unit_of_measurement ?? ""}`; + return html` + + + + ${picture + ? this.renderPicture(picture) + : this.renderIcon(stateObj, icon)} + ${this.renderBadge(stateObj)} + ${this.renderStateInfo(stateObj, appearance, name, stateDisplay)}; + +
+ +
+
+
+ `; + } + + renderIcon(stateObj: HassEntity, icon?: string): TemplateResult { + const active = isActive(stateObj); + const iconStyle = {}; + const iconColor = this._config?.icon_color; + if (iconColor) { + const iconRgbColor = computeRgbColor(iconColor); + iconStyle["--icon-color"] = `rgb(${iconRgbColor})`; + iconStyle["--shape-color"] = `rgba(${iconRgbColor}, 0.2)`; + } + return html` + + + + `; + } + + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + mushroom-state-item { + cursor: pointer; } - - const rtl = computeRTL(this.hass); - - const sliderStyle = {}; - const iconColor = this._config?.icon_color; - if (iconColor) { - const iconRgbColor = computeRgbColor(iconColor); - sliderStyle["--slider-color"] = `rgb(${iconRgbColor})`; - sliderStyle["--slider-bg-color"] = `rgba(${iconRgbColor}, 0.2)`; + mushroom-shape-icon { + --icon-color: rgb(var(--rgb-state-number)); + --shape-color: rgba(var(--rgb-state-number), 0.2); } - - return html` - - - - ${picture ? this.renderPicture(picture) : this.renderIcon(stateObj, icon)} - ${this.renderBadge(stateObj)} - ${this.renderStateInfo(stateObj, appearance, name, stateDisplay)}; - -
- -
-
-
- `; - } - - renderIcon(stateObj: HassEntity, icon?: string): TemplateResult { - const active = isActive(stateObj); - const iconStyle = {}; - const iconColor = this._config?.icon_color; - if (iconColor) { - const iconRgbColor = computeRgbColor(iconColor); - iconStyle["--icon-color"] = `rgb(${iconRgbColor})`; - iconStyle["--shape-color"] = `rgba(${iconRgbColor}, 0.2)`; + mushroom-number-value-control { + flex: 1; } - return html` - - - - `; - } - - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - mushroom-state-item { - cursor: pointer; - } - mushroom-shape-icon { - --icon-color: rgb(var(--rgb-state-number)); - --shape-color: rgba(var(--rgb-state-number), 0.2); - } - mushroom-number-value-control { - flex: 1; - } - `, - ]; - } + `, + ]; + } } diff --git a/src/cards/person-card/person-card-config.ts b/src/cards/person-card/person-card-config.ts index 1ef7ccd53..490ffe735 100644 --- a/src/cards/person-card/person-card-config.ts +++ b/src/cards/person-card/person-card-config.ts @@ -1,19 +1,29 @@ import { assign, boolean, object, optional } from "superstruct"; import { LovelaceCardConfig } from "../../ha"; -import { ActionsSharedConfig, actionsSharedConfigStruct } from "../../shared/config/actions-config"; import { - AppearanceSharedConfig, - appearanceSharedConfigStruct, + ActionsSharedConfig, + actionsSharedConfigStruct, +} from "../../shared/config/actions-config"; +import { + AppearanceSharedConfig, + appearanceSharedConfigStruct, } from "../../shared/config/appearance-config"; -import { EntitySharedConfig, entitySharedConfigStruct } from "../../shared/config/entity-config"; +import { + EntitySharedConfig, + entitySharedConfigStruct, +} from "../../shared/config/entity-config"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; export type PersonCardConfig = LovelaceCardConfig & - EntitySharedConfig & - AppearanceSharedConfig & - ActionsSharedConfig; + EntitySharedConfig & + AppearanceSharedConfig & + ActionsSharedConfig; export const personCardConfigStruct = assign( - lovelaceCardConfigStruct, - assign(entitySharedConfigStruct, appearanceSharedConfigStruct, actionsSharedConfigStruct) + lovelaceCardConfigStruct, + assign( + entitySharedConfigStruct, + appearanceSharedConfigStruct, + actionsSharedConfigStruct + ) ); diff --git a/src/cards/person-card/person-card-editor.ts b/src/cards/person-card/person-card-editor.ts index d9a7c560e..2f4efa97c 100644 --- a/src/cards/person-card/person-card-editor.ts +++ b/src/cards/person-card/person-card-editor.ts @@ -13,56 +13,68 @@ import { loadHaComponents } from "../../utils/loader"; import { PERSON_CARD_EDITOR_NAME, PERSON_ENTITY_DOMAINS } from "./const"; import { PersonCardConfig, personCardConfigStruct } from "./person-card-config"; -const actions: UiAction[] = ["more-info", "navigate", "url", "call-service", "assist", "none"]; +const actions: UiAction[] = [ + "more-info", + "navigate", + "url", + "call-service", + "assist", + "none", +]; const SCHEMA: HaFormSchema[] = [ - { name: "entity", selector: { entity: { domain: PERSON_ENTITY_DOMAINS } } }, - { name: "name", selector: { text: {} } }, - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - ...APPEARANCE_FORM_SCHEMA, - ...computeActionsFormSchema(actions), + { name: "entity", selector: { entity: { domain: PERSON_ENTITY_DOMAINS } } }, + { name: "name", selector: { text: {} } }, + { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, + ...APPEARANCE_FORM_SCHEMA, + ...computeActionsFormSchema(actions), ]; @customElement(PERSON_CARD_EDITOR_NAME) -export class SwitchCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: PersonCardConfig; - - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } +export class SwitchCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: PersonCardConfig; - public setConfig(config: PersonCardConfig): void { - assert(config, personCardConfigStruct); - this._config = config; - } + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + public setConfig(config: PersonCardConfig): void { + assert(config, personCardConfigStruct); + this._config = config; + } - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - protected render() { - if (!this.hass || !this._config) { - return nothing; - } - - return html` - - `; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/person-card/person-card.ts b/src/cards/person-card/person-card.ts index 8cef3d0ba..4c2513c43 100644 --- a/src/cards/person-card/person-card.ts +++ b/src/cards/person-card/person-card.ts @@ -4,15 +4,15 @@ import { customElement } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - computeRTL, - handleAction, - hasAction, - HomeAssistant, - isAvailable, - LovelaceCard, - LovelaceCardEditor, + actionHandler, + ActionHandlerEvent, + computeRTL, + handleAction, + hasAction, + HomeAssistant, + isAvailable, + LovelaceCard, + LovelaceCardEditor, } from "../../ha"; import "../../shared/badge-icon"; import "../../shared/card"; @@ -25,111 +25,128 @@ import { MushroomBaseCard } from "../../utils/base-card"; import { cardStyle } from "../../utils/card-styles"; import { registerCustomCard } from "../../utils/custom-cards"; import { computeEntityPicture } from "../../utils/info"; -import { PERSON_CARD_EDITOR_NAME, PERSON_CARD_NAME, PERSON_ENTITY_DOMAINS } from "./const"; +import { + PERSON_CARD_EDITOR_NAME, + PERSON_CARD_NAME, + PERSON_ENTITY_DOMAINS, +} from "./const"; import { PersonCardConfig } from "./person-card-config"; import { getStateColor, getStateIcon } from "./utils"; registerCustomCard({ - type: PERSON_CARD_NAME, - name: "Mushroom Person Card", - description: "Card for person entity", + type: PERSON_CARD_NAME, + name: "Mushroom Person Card", + description: "Card for person entity", }); @customElement(PERSON_CARD_NAME) -export class PersonCard extends MushroomBaseCard implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./person-card-editor"); - return document.createElement(PERSON_CARD_EDITOR_NAME) as LovelaceCardEditor; - } +export class PersonCard + extends MushroomBaseCard + implements LovelaceCard +{ + public static async getConfigElement(): Promise { + await import("./person-card-editor"); + return document.createElement( + PERSON_CARD_EDITOR_NAME + ) as LovelaceCardEditor; + } - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - const people = entities.filter((e) => PERSON_ENTITY_DOMAINS.includes(e.split(".")[0])); - return { - type: `custom:${PERSON_CARD_NAME}`, - entity: people[0], - }; - } + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + const people = entities.filter((e) => + PERSON_ENTITY_DOMAINS.includes(e.split(".")[0]) + ); + return { + type: `custom:${PERSON_CARD_NAME}`, + entity: people[0], + }; + } - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + protected render() { + if (!this._config || !this.hass || !this._config.entity) { + return nothing; } - protected render() { - if (!this._config || !this.hass || !this._config.entity) { - return nothing; - } + const stateObj = this._stateObj; - const stateObj = this._stateObj; + if (!stateObj) { + return this.renderNotFound(this._config); + } - if (!stateObj) { - return this.renderNotFound(this._config); - } + const name = this._config.name || stateObj.attributes.friendly_name || ""; + const icon = this._config.icon; + const appearance = computeAppearance(this._config); + const picture = computeEntityPicture(stateObj, appearance.icon_type); - const name = this._config.name || stateObj.attributes.friendly_name || ""; - const icon = this._config.icon; - const appearance = computeAppearance(this._config); - const picture = computeEntityPicture(stateObj, appearance.icon_type); + const rtl = computeRTL(this.hass); - const rtl = computeRTL(this.hass); + return html` + + + + ${picture + ? this.renderPicture(picture) + : this.renderIcon(stateObj, icon)} + ${this.renderBadge(stateObj)} + ${this.renderStateInfo(stateObj, appearance, name)}; + + + + `; + } - return html` - - - - ${picture ? this.renderPicture(picture) : this.renderIcon(stateObj, icon)} - ${this.renderBadge(stateObj)} - ${this.renderStateInfo(stateObj, appearance, name)}; - - - - `; - } + renderStateBadge(stateObj: HassEntity) { + const zones = Object.values(this.hass.states).filter((stateObj) => + stateObj.entity_id.startsWith("zone.") + ); + const icon = getStateIcon(stateObj, zones); + const color = getStateColor(stateObj, zones); - renderStateBadge(stateObj: HassEntity) { - const zones = Object.values(this.hass.states).filter((stateObj) => - stateObj.entity_id.startsWith("zone.") - ); - const icon = getStateIcon(stateObj, zones); - const color = getStateColor(stateObj, zones); + return html` + + `; + } - return html` - - `; + renderBadge(stateObj: HassEntity) { + const unavailable = !isAvailable(stateObj); + if (unavailable) { + return super.renderBadge(stateObj); + } else { + return this.renderStateBadge(stateObj); } + } - renderBadge(stateObj: HassEntity) { - const unavailable = !isAvailable(stateObj); - if (unavailable) { - return super.renderBadge(stateObj); - } else { - return this.renderStateBadge(stateObj); + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + mushroom-state-item { + cursor: pointer; } - } - - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - mushroom-state-item { - cursor: pointer; - } - `, - ]; - } + `, + ]; + } } diff --git a/src/cards/person-card/utils.ts b/src/cards/person-card/utils.ts index 9ee5e731e..27502fed5 100644 --- a/src/cards/person-card/utils.ts +++ b/src/cards/person-card/utils.ts @@ -2,35 +2,35 @@ import { HassEntity } from "home-assistant-js-websocket"; import { UNKNOWN } from "../../ha"; export function getStateIcon(stateObj: HassEntity, zones: HassEntity[]) { - const state = stateObj.state; - if (state === UNKNOWN) { - return "mdi:help"; - } else if (state === "not_home") { - return "mdi:home-export-outline"; - } else if (state === "home") { - return "mdi:home"; - } + const state = stateObj.state; + if (state === UNKNOWN) { + return "mdi:help"; + } else if (state === "not_home") { + return "mdi:home-export-outline"; + } else if (state === "home") { + return "mdi:home"; + } - const zone = zones.find((z) => state === z.attributes.friendly_name); - if (zone && zone.attributes.icon) { - return zone.attributes.icon; - } + const zone = zones.find((z) => state === z.attributes.friendly_name); + if (zone && zone.attributes.icon) { + return zone.attributes.icon; + } - return "mdi:home"; + return "mdi:home"; } export function getStateColor(stateObj: HassEntity, zones: HassEntity[]) { - const state = stateObj.state; - if (state === UNKNOWN) { - return "var(--rgb-state-person-unknown)"; - } else if (state === "not_home") { - return "var(--rgb-state-person-not-home)"; - } else if (state === "home") { - return "var(--rgb-state-person-home)"; - } - const isInZone = zones.some((z) => state === z.attributes.friendly_name); - if (isInZone) { - return "var(--rgb-state-person-zone)"; - } + const state = stateObj.state; + if (state === UNKNOWN) { + return "var(--rgb-state-person-unknown)"; + } else if (state === "not_home") { + return "var(--rgb-state-person-not-home)"; + } else if (state === "home") { return "var(--rgb-state-person-home)"; + } + const isInZone = zones.some((z) => state === z.attributes.friendly_name); + if (isInZone) { + return "var(--rgb-state-person-zone)"; + } + return "var(--rgb-state-person-home)"; } diff --git a/src/cards/select-card/controls/select-option-control.ts b/src/cards/select-card/controls/select-option-control.ts index dc42dd850..dd4cf1b63 100644 --- a/src/cards/select-card/controls/select-option-control.ts +++ b/src/cards/select-card/controls/select-option-control.ts @@ -7,69 +7,69 @@ import { getCurrentOption, getOptions } from "../utils"; @customElement("mushroom-select-option-control") export class SelectOptionControl extends LitElement { - @property() public hass!: HomeAssistant; + @property() public hass!: HomeAssistant; - @property({ attribute: false }) public entity!: HassEntity; + @property({ attribute: false }) public entity!: HassEntity; - _selectChanged(ev) { - const value = ev.target.value; + _selectChanged(ev) { + const value = ev.target.value; - const currentValue = getCurrentOption(this.entity); + const currentValue = getCurrentOption(this.entity); - if (value && value !== currentValue) { - this._setValue(value); - } + if (value && value !== currentValue) { + this._setValue(value); } + } - _setValue(option) { - const entityId = this.entity.entity_id; - const domain = entityId.split(".")[0]; + _setValue(option) { + const entityId = this.entity.entity_id; + const domain = entityId.split(".")[0]; - this.hass.callService(domain, "select_option", { - entity_id: this.entity.entity_id, - option: option, - }); - } + this.hass.callService(domain, "select_option", { + entity_id: this.entity.entity_id, + option: option, + }); + } - render() { - const value = getCurrentOption(this.entity); + render() { + const value = getCurrentOption(this.entity); - const options = getOptions(this.entity); + const options = getOptions(this.entity); - return html` - e.stopPropagation()} - .value=${value ?? ""} - naturalMenuWidth - fixedMenuPosition - > - ${options.map((option) => { - return html` - - ${this.hass.formatEntityState - ? this.hass.formatEntityState(this.entity, option) - : computeStateDisplay( - this.hass.localize, - this.entity, - this.hass.locale, - this.hass.config, - this.hass.entities, - option - )} - - `; - })} - - `; - } + return html` + e.stopPropagation()} + .value=${value ?? ""} + naturalMenuWidth + fixedMenuPosition + > + ${options.map((option) => { + return html` + + ${this.hass.formatEntityState + ? this.hass.formatEntityState(this.entity, option) + : computeStateDisplay( + this.hass.localize, + this.entity, + this.hass.locale, + this.hass.config, + this.hass.entities, + option + )} + + `; + })} + + `; + } - static get styles(): CSSResultGroup { - return css` - mushroom-select { - --select-height: 42px; - width: 100%; - } - `; - } + static get styles(): CSSResultGroup { + return css` + mushroom-select { + --select-height: 42px; + width: 100%; + } + `; + } } diff --git a/src/cards/select-card/select-card-config.ts b/src/cards/select-card/select-card-config.ts index 73c255209..38d7ea03f 100644 --- a/src/cards/select-card/select-card-config.ts +++ b/src/cards/select-card/select-card-config.ts @@ -1,24 +1,34 @@ import { assign, boolean, object, optional, string } from "superstruct"; -import { ActionsSharedConfig, actionsSharedConfigStruct } from "../../shared/config/actions-config"; import { - appearanceSharedConfigStruct, - AppearanceSharedConfig, + ActionsSharedConfig, + actionsSharedConfigStruct, +} from "../../shared/config/actions-config"; +import { + appearanceSharedConfigStruct, + AppearanceSharedConfig, } from "../../shared/config/appearance-config"; -import { entitySharedConfigStruct, EntitySharedConfig } from "../../shared/config/entity-config"; +import { + entitySharedConfigStruct, + EntitySharedConfig, +} from "../../shared/config/entity-config"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; import { LovelaceCardConfig } from "../../ha"; export type SelectCardConfig = LovelaceCardConfig & - EntitySharedConfig & - AppearanceSharedConfig & - ActionsSharedConfig & { - icon_color?: string; - }; + EntitySharedConfig & + AppearanceSharedConfig & + ActionsSharedConfig & { + icon_color?: string; + }; export const selectCardConfigStruct = assign( - lovelaceCardConfigStruct, - assign(entitySharedConfigStruct, appearanceSharedConfigStruct, actionsSharedConfigStruct), - object({ - icon_color: optional(string()), - }) + lovelaceCardConfigStruct, + assign( + entitySharedConfigStruct, + appearanceSharedConfigStruct, + actionsSharedConfigStruct + ), + object({ + icon_color: optional(string()), + }) ); diff --git a/src/cards/select-card/select-card-editor.ts b/src/cards/select-card/select-card-editor.ts index 41114929f..3f2a7ee3b 100644 --- a/src/cards/select-card/select-card-editor.ts +++ b/src/cards/select-card/select-card-editor.ts @@ -13,63 +13,79 @@ import { loadHaComponents } from "../../utils/loader"; import { SELECT_CARD_EDITOR_NAME, SELECT_ENTITY_DOMAINS } from "./const"; import { SelectCardConfig, selectCardConfigStruct } from "./select-card-config"; -const actions: UiAction[] = ["more-info", "navigate", "url", "call-service", "assist", "none"]; +const actions: UiAction[] = [ + "more-info", + "navigate", + "url", + "call-service", + "assist", + "none", +]; const SCHEMA: HaFormSchema[] = [ - { name: "entity", selector: { entity: { domain: SELECT_ENTITY_DOMAINS } } }, - { name: "name", selector: { text: {} } }, - { - type: "grid", - name: "", - schema: [ - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - { name: "icon_color", selector: { mush_color: {} } }, - ], - }, - ...APPEARANCE_FORM_SCHEMA, - ...computeActionsFormSchema(actions), + { name: "entity", selector: { entity: { domain: SELECT_ENTITY_DOMAINS } } }, + { name: "name", selector: { text: {} } }, + { + type: "grid", + name: "", + schema: [ + { + name: "icon", + selector: { icon: {} }, + context: { icon_entity: "entity" }, + }, + { name: "icon_color", selector: { mush_color: {} } }, + ], + }, + ...APPEARANCE_FORM_SCHEMA, + ...computeActionsFormSchema(actions), ]; @customElement(SELECT_CARD_EDITOR_NAME) -export class SelectCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: SelectCardConfig; - - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } +export class SelectCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: SelectCardConfig; - public setConfig(config: SelectCardConfig): void { - assert(config, selectCardConfigStruct); - this._config = config; - } + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + public setConfig(config: SelectCardConfig): void { + assert(config, selectCardConfigStruct); + this._config = config; + } - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - protected render() { - if (!this.hass || !this._config) { - return nothing; - } - - return html` - - `; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/select-card/select-card.ts b/src/cards/select-card/select-card.ts index b81b85c00..2b2a010ac 100644 --- a/src/cards/select-card/select-card.ts +++ b/src/cards/select-card/select-card.ts @@ -4,15 +4,15 @@ import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - computeRTL, - handleAction, - hasAction, - HomeAssistant, - isActive, - LovelaceCard, - LovelaceCardEditor, + actionHandler, + ActionHandlerEvent, + computeRTL, + handleAction, + hasAction, + HomeAssistant, + isActive, + LovelaceCard, + LovelaceCardEditor, } from "../../ha"; import "../../shared/badge-icon"; import "../../shared/card"; @@ -26,135 +26,156 @@ import { cardStyle } from "../../utils/card-styles"; import { computeRgbColor } from "../../utils/colors"; import { registerCustomCard } from "../../utils/custom-cards"; import { computeEntityPicture } from "../../utils/info"; -import { SELECT_CARD_EDITOR_NAME, SELECT_CARD_NAME, SELECT_ENTITY_DOMAINS } from "./const"; +import { + SELECT_CARD_EDITOR_NAME, + SELECT_CARD_NAME, + SELECT_ENTITY_DOMAINS, +} from "./const"; import "./controls/select-option-control"; import { SelectCardConfig } from "./select-card-config"; registerCustomCard({ - type: SELECT_CARD_NAME, - name: "Mushroom Select Card", - description: "Card for select and input_select entities", + type: SELECT_CARD_NAME, + name: "Mushroom Select Card", + description: "Card for select and input_select entities", }); @customElement(SELECT_CARD_NAME) -export class SelectCard extends MushroomBaseCard implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./select-card-editor"); - return document.createElement(SELECT_CARD_EDITOR_NAME) as LovelaceCardEditor; - } +export class SelectCard + extends MushroomBaseCard + implements LovelaceCard +{ + public static async getConfigElement(): Promise { + await import("./select-card-editor"); + return document.createElement( + SELECT_CARD_EDITOR_NAME + ) as LovelaceCardEditor; + } - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - const selects = entities.filter((e) => SELECT_ENTITY_DOMAINS.includes(e.split(".")[0])); - return { - type: `custom:${SELECT_CARD_NAME}`, - entity: selects[0], - }; - } + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + const selects = entities.filter((e) => + SELECT_ENTITY_DOMAINS.includes(e.split(".")[0]) + ); + return { + type: `custom:${SELECT_CARD_NAME}`, + entity: selects[0], + }; + } - protected get hasControls(): boolean { - return true; - } + protected get hasControls(): boolean { + return true; + } - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + protected render() { + if (!this._config || !this.hass || !this._config.entity) { + return nothing; } - protected render() { - if (!this._config || !this.hass || !this._config.entity) { - return nothing; - } + const stateObj = this._stateObj; - const stateObj = this._stateObj; + if (!stateObj) { + return this.renderNotFound(this._config); + } - if (!stateObj) { - return this.renderNotFound(this._config); - } + const name = this._config.name || stateObj.attributes.friendly_name || ""; + const icon = this._config.icon; + const appearance = computeAppearance(this._config); - const name = this._config.name || stateObj.attributes.friendly_name || ""; - const icon = this._config.icon; - const appearance = computeAppearance(this._config); + const picture = computeEntityPicture(stateObj, appearance.icon_type); - const picture = computeEntityPicture(stateObj, appearance.icon_type); + const rtl = computeRTL(this.hass); + const iconColor = this._config?.icon_color; - const rtl = computeRTL(this.hass); - const iconColor = this._config?.icon_color; + const selectStyle = {}; + if (iconColor) { + const iconRgbColor = computeRgbColor(iconColor); + selectStyle["--mdc-theme-primary"] = `rgb(${iconRgbColor})`; + } - const selectStyle = {}; - if (iconColor) { - const iconRgbColor = computeRgbColor(iconColor); - selectStyle["--mdc-theme-primary"] = `rgb(${iconRgbColor})`; - } + return html` + + + + ${picture + ? this.renderPicture(picture) + : this.renderIcon(stateObj, icon)} + ${this.renderBadge(stateObj)} + ${this.renderStateInfo(stateObj, appearance, name)}; + +
+ +
+
+
+ `; + } - return html` - - - - ${picture ? this.renderPicture(picture) : this.renderIcon(stateObj, icon)} - ${this.renderBadge(stateObj)} - ${this.renderStateInfo(stateObj, appearance, name)}; - -
- -
-
-
- `; + renderIcon(stateObj: HassEntity, icon?: string): TemplateResult { + const active = isActive(stateObj); + const iconStyle = {}; + const iconColor = this._config?.icon_color; + if (iconColor) { + const iconRgbColor = computeRgbColor(iconColor); + iconStyle["--icon-color"] = `rgb(${iconRgbColor})`; + iconStyle["--shape-color"] = `rgba(${iconRgbColor}, 0.2)`; } + return html` + + + + `; + } - renderIcon(stateObj: HassEntity, icon?: string): TemplateResult { - const active = isActive(stateObj); - const iconStyle = {}; - const iconColor = this._config?.icon_color; - if (iconColor) { - const iconRgbColor = computeRgbColor(iconColor); - iconStyle["--icon-color"] = `rgb(${iconRgbColor})`; - iconStyle["--shape-color"] = `rgba(${iconRgbColor}, 0.2)`; + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + .actions { + overflow: visible; + display: block; } - return html` - - - - `; - } - - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - .actions { - overflow: visible; - display: block; - } - mushroom-state-item { - cursor: pointer; - } - mushroom-shape-icon { - --icon-color: rgb(var(--rgb-state-entity)); - --shape-color: rgba(var(--rgb-state-entity), 0.2); - } - mushroom-select-option-control { - flex: 1; - --mdc-theme-primary: rgb(var(--rgb-state-entity)); - } - `, - ]; - } + mushroom-state-item { + cursor: pointer; + } + mushroom-shape-icon { + --icon-color: rgb(var(--rgb-state-entity)); + --shape-color: rgba(var(--rgb-state-entity), 0.2); + } + mushroom-select-option-control { + flex: 1; + --mdc-theme-primary: rgb(var(--rgb-state-entity)); + } + `, + ]; + } } diff --git a/src/cards/select-card/utils.ts b/src/cards/select-card/utils.ts index 9d7844181..8dc657318 100644 --- a/src/cards/select-card/utils.ts +++ b/src/cards/select-card/utils.ts @@ -1,9 +1,9 @@ import { HassEntity } from "home-assistant-js-websocket"; export function getCurrentOption(stateObj: HassEntity) { - return stateObj.state != null ? stateObj.state : undefined; + return stateObj.state != null ? stateObj.state : undefined; } export function getOptions(stateObj: HassEntity) { - return stateObj.attributes.options; + return stateObj.attributes.options; } diff --git a/src/cards/template-card/template-card-config.ts b/src/cards/template-card/template-card-config.ts index 304f174d6..26b30d9d2 100644 --- a/src/cards/template-card/template-card-config.ts +++ b/src/cards/template-card/template-card-config.ts @@ -1,40 +1,51 @@ -import { array, assign, boolean, object, optional, string, union } from "superstruct"; +import { + array, + assign, + boolean, + object, + optional, + string, + union, +} from "superstruct"; import { LovelaceCardConfig } from "../../ha"; -import { ActionsSharedConfig, actionsSharedConfigStruct } from "../../shared/config/actions-config"; import { - AppearanceSharedConfig, - appearanceSharedConfigStruct, + ActionsSharedConfig, + actionsSharedConfigStruct, +} from "../../shared/config/actions-config"; +import { + AppearanceSharedConfig, + appearanceSharedConfigStruct, } from "../../shared/config/appearance-config"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; export type TemplateCardConfig = LovelaceCardConfig & - AppearanceSharedConfig & - ActionsSharedConfig & { - entity?: string; - icon?: string; - icon_color?: string; - primary?: string; - secondary?: string; - badge_icon?: string; - badge_color?: string; - picture?: string; - multiline_secondary?: boolean; - entity_id?: string | string[]; - }; + AppearanceSharedConfig & + ActionsSharedConfig & { + entity?: string; + icon?: string; + icon_color?: string; + primary?: string; + secondary?: string; + badge_icon?: string; + badge_color?: string; + picture?: string; + multiline_secondary?: boolean; + entity_id?: string | string[]; + }; export const templateCardConfigStruct = assign( - lovelaceCardConfigStruct, - assign(appearanceSharedConfigStruct, actionsSharedConfigStruct), - object({ - entity: optional(string()), - icon: optional(string()), - icon_color: optional(string()), - primary: optional(string()), - secondary: optional(string()), - badge_icon: optional(string()), - badge_color: optional(string()), - picture: optional(string()), - multiline_secondary: optional(boolean()), - entity_id: optional(union([string(), array(string())])), - }) + lovelaceCardConfigStruct, + assign(appearanceSharedConfigStruct, actionsSharedConfigStruct), + object({ + entity: optional(string()), + icon: optional(string()), + icon_color: optional(string()), + primary: optional(string()), + secondary: optional(string()), + badge_icon: optional(string()), + badge_color: optional(string()), + picture: optional(string()), + multiline_secondary: optional(boolean()), + entity_id: optional(union([string(), array(string())])), + }) ); diff --git a/src/cards/template-card/template-card-editor.ts b/src/cards/template-card/template-card-editor.ts index c1d059579..9297bfc30 100644 --- a/src/cards/template-card/template-card-editor.ts +++ b/src/cards/template-card/template-card-editor.ts @@ -9,108 +9,116 @@ import { GENERIC_LABELS } from "../../utils/form/generic-fields"; import { HaFormSchema } from "../../utils/form/ha-form"; import { loadHaComponents } from "../../utils/loader"; import { TEMPLATE_CARD_EDITOR_NAME } from "./const"; -import { TemplateCardConfig, templateCardConfigStruct } from "./template-card-config"; +import { + TemplateCardConfig, + templateCardConfigStruct, +} from "./template-card-config"; export const TEMPLATE_LABELS = [ - "badge_icon", - "badge_color", - "content", - "primary", - "secondary", - "multiline_secondary", - "picture", + "badge_icon", + "badge_color", + "content", + "primary", + "secondary", + "multiline_secondary", + "picture", ]; const SCHEMA: HaFormSchema[] = [ - { name: "entity", selector: { entity: {} } }, - { - name: "icon", - selector: { template: {} }, - }, - { - name: "icon_color", - selector: { template: {} }, - }, - { - name: "primary", - selector: { template: {} }, - }, - { - name: "secondary", - selector: { template: {} }, - }, - { - name: "badge_icon", - selector: { template: {} }, - }, - { - name: "badge_color", - selector: { template: {} }, - }, - { - name: "picture", - selector: { template: {} }, - }, - { - type: "grid", - name: "", - schema: [ - { name: "layout", selector: { mush_layout: {} } }, - { name: "fill_container", selector: { boolean: {} } }, - { name: "multiline_secondary", selector: { boolean: {} } }, - ], - }, - ...computeActionsFormSchema(), + { name: "entity", selector: { entity: {} } }, + { + name: "icon", + selector: { template: {} }, + }, + { + name: "icon_color", + selector: { template: {} }, + }, + { + name: "primary", + selector: { template: {} }, + }, + { + name: "secondary", + selector: { template: {} }, + }, + { + name: "badge_icon", + selector: { template: {} }, + }, + { + name: "badge_color", + selector: { template: {} }, + }, + { + name: "picture", + selector: { template: {} }, + }, + { + type: "grid", + name: "", + schema: [ + { name: "layout", selector: { mush_layout: {} } }, + { name: "fill_container", selector: { boolean: {} } }, + { name: "multiline_secondary", selector: { boolean: {} } }, + ], + }, + ...computeActionsFormSchema(), ]; @customElement(TEMPLATE_CARD_EDITOR_NAME) -export class TemplateCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: TemplateCardConfig; +export class TemplateCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: TemplateCardConfig; - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } - - public setConfig(config: TemplateCardConfig): void { - assert(config, templateCardConfigStruct); - this._config = config; - } - - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } - if (schema.name === "entity") { - return `${this.hass!.localize( - "ui.panel.lovelace.editor.card.generic.entity" - )} (${customLocalize("editor.card.template.entity_extra")})`; - } - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - if (TEMPLATE_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.template.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + public setConfig(config: TemplateCardConfig): void { + assert(config, templateCardConfigStruct); + this._config = config; + } - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - return html` - - `; + if (schema.name === "entity") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.entity" + )} (${customLocalize("editor.card.template.entity_extra")})`; } + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); + } + if (TEMPLATE_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.template.${schema.name}`); + } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/template-card/template-card.ts b/src/cards/template-card/template-card.ts index ff2151aea..fb462c413 100644 --- a/src/cards/template-card/template-card.ts +++ b/src/cards/template-card/template-card.ts @@ -1,20 +1,27 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { css, CSSResultGroup, html, nothing, PropertyValues, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + nothing, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - computeRTL, - handleAction, - hasAction, - HomeAssistant, - LovelaceCard, - LovelaceCardEditor, - LovelaceLayoutOptions, - RenderTemplateResult, - subscribeRenderTemplate, + actionHandler, + ActionHandlerEvent, + computeRTL, + handleAction, + hasAction, + HomeAssistant, + LovelaceCard, + LovelaceCardEditor, + LovelaceLayoutOptions, + RenderTemplateResult, + subscribeRenderTemplate, } from "../../ha"; import "../../shared/shape-icon"; import "../../shared/state-info"; @@ -30,337 +37,353 @@ import { TEMPLATE_CARD_EDITOR_NAME, TEMPLATE_CARD_NAME } from "./const"; import { TemplateCardConfig } from "./template-card-config"; registerCustomCard({ - type: TEMPLATE_CARD_NAME, - name: "Mushroom Template Card", - description: "Card for custom rendering with templates", + type: TEMPLATE_CARD_NAME, + name: "Mushroom Template Card", + description: "Card for custom rendering with templates", }); const TEMPLATE_KEYS = [ - "icon", - "icon_color", - "badge_color", - "badge_icon", - "primary", - "secondary", - "picture", + "icon", + "icon_color", + "badge_color", + "badge_icon", + "primary", + "secondary", + "picture", ] as const; type TemplateKey = (typeof TEMPLATE_KEYS)[number]; @customElement(TEMPLATE_CARD_NAME) export class TemplateCard extends MushroomBaseElement implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./template-card-editor"); - return document.createElement(TEMPLATE_CARD_EDITOR_NAME) as LovelaceCardEditor; - } - - public static async getStubConfig(_hass: HomeAssistant): Promise { - return { - type: `custom:${TEMPLATE_CARD_NAME}`, - primary: "Hello, {{user}}", - secondary: "How are you?", - icon: "mdi:home", - }; - } - - @state() private _config?: TemplateCardConfig; - - @state() private _templateResults: Partial< - Record - > = {}; - - @state() private _unsubRenderTemplates: Map> = new Map(); - - @property({ reflect: true, type: String }) - public layout: string | undefined; - - // For backward compatibility (version < 2024.7) - @property({ attribute: "in-grid", reflect: true, type: Boolean }) - protected _inGrid = false; - - public getCardSize(): number | Promise { - let height = 1; - if (!this._config) return height; - const appearance = computeAppearance(this._config); - if (appearance.layout === "vertical") { - height += 1; - } - return height; + public static async getConfigElement(): Promise { + await import("./template-card-editor"); + return document.createElement( + TEMPLATE_CARD_EDITOR_NAME + ) as LovelaceCardEditor; + } + + public static async getStubConfig( + _hass: HomeAssistant + ): Promise { + return { + type: `custom:${TEMPLATE_CARD_NAME}`, + primary: "Hello, {{user}}", + secondary: "How are you?", + icon: "mdi:home", + }; + } + + @state() private _config?: TemplateCardConfig; + + @state() private _templateResults: Partial< + Record + > = {}; + + @state() private _unsubRenderTemplates: Map< + TemplateKey, + Promise + > = new Map(); + + @property({ reflect: true, type: String }) + public layout: string | undefined; + + // For backward compatibility (version < 2024.7) + @property({ attribute: "in-grid", reflect: true, type: Boolean }) + protected _inGrid = false; + + public getCardSize(): number | Promise { + let height = 1; + if (!this._config) return height; + const appearance = computeAppearance(this._config); + if (appearance.layout === "vertical") { + height += 1; } - - // For backward compatibility - public getGridSize(): [number | undefined, number | undefined] { - const { grid_columns, grid_rows } = this.getLayoutOptions(); - return [grid_columns, grid_rows]; - } - - public getLayoutOptions(): LovelaceLayoutOptions { - this._inGrid = true; - const options: LovelaceLayoutOptions = { - grid_columns: 2, - grid_rows: 1, - }; - if (!this._config) return options; - const appearance = computeAppearance(this._config); - if (appearance.layout === "vertical") { - options.grid_rows! += 1; - } - if (appearance.layout === "horizontal") { - options.grid_columns = 4; - } - if (this._config?.multiline_secondary) { - options.grid_rows = undefined; - } - return options; - } - - setConfig(config: TemplateCardConfig): void { - TEMPLATE_KEYS.forEach((key) => { - if (this._config?.[key] !== config[key] || this._config?.entity != config.entity) { - this._tryDisconnectKey(key); - } - }); - this._config = { - tap_action: { - action: "toggle", - }, - hold_action: { - action: "more-info", - }, - ...config, - }; - } - - public connectedCallback() { - super.connectedCallback(); - this._tryConnect(); + return height; + } + + // For backward compatibility + public getGridSize(): [number | undefined, number | undefined] { + const { grid_columns, grid_rows } = this.getLayoutOptions(); + return [grid_columns, grid_rows]; + } + + public getLayoutOptions(): LovelaceLayoutOptions { + this._inGrid = true; + const options: LovelaceLayoutOptions = { + grid_columns: 2, + grid_rows: 1, + }; + if (!this._config) return options; + const appearance = computeAppearance(this._config); + if (appearance.layout === "vertical") { + options.grid_rows! += 1; } - - public disconnectedCallback() { - this._tryDisconnect(); + if (appearance.layout === "horizontal") { + options.grid_columns = 4; } - - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); + if (this._config?.multiline_secondary) { + options.grid_rows = undefined; } - - public isTemplate(key: TemplateKey) { - const value = this._config?.[key]; - return value?.includes("{"); + return options; + } + + setConfig(config: TemplateCardConfig): void { + TEMPLATE_KEYS.forEach((key) => { + if ( + this._config?.[key] !== config[key] || + this._config?.entity != config.entity + ) { + this._tryDisconnectKey(key); + } + }); + this._config = { + tap_action: { + action: "toggle", + }, + hold_action: { + action: "more-info", + }, + ...config, + }; + } + + public connectedCallback() { + super.connectedCallback(); + this._tryConnect(); + } + + public disconnectedCallback() { + this._tryDisconnect(); + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + public isTemplate(key: TemplateKey) { + const value = this._config?.[key]; + return value?.includes("{"); + } + + private getValue(key: TemplateKey) { + return this.isTemplate(key) + ? this._templateResults[key]?.result?.toString() + : this._config?.[key]; + } + + protected render() { + if (!this._config || !this.hass) { + return nothing; } - private getValue(key: TemplateKey) { - return this.isTemplate(key) - ? this._templateResults[key]?.result?.toString() - : this._config?.[key]; + const icon = this.getValue("icon"); + const iconColor = this.getValue("icon_color"); + const badgeIcon = this.getValue("badge_icon"); + const badgeColor = this.getValue("badge_color"); + const primary = this.getValue("primary"); + const secondary = this.getValue("secondary"); + const picture = this.getValue("picture"); + + const multiline_secondary = this._config.multiline_secondary; + + const rtl = computeRTL(this.hass); + + const appearance = computeAppearance({ + fill_container: this._config.fill_container, + layout: this._config.layout, + icon_type: Boolean(picture) + ? "entity-picture" + : Boolean(icon) + ? "icon" + : "none", + primary_info: Boolean(primary) ? "name" : "none", + secondary_info: Boolean(secondary) ? "state" : "none", + }); + + const weatherSvg = getWeatherSvgIcon(icon); + + return html` + + + + ${picture + ? this.renderPicture(picture) + : weatherSvg + ? html`
${weatherSvg}
` + : icon + ? this.renderIcon(icon, iconColor) + : nothing} + ${(icon || picture) && badgeIcon + ? this.renderBadgeIcon(badgeIcon, badgeColor) + : undefined} + +
+
+
+ `; + } + + renderPicture(picture: string): TemplateResult { + return html` + + `; + } + + renderIcon(icon: string, iconColor?: string) { + const iconStyle = {}; + if (iconColor) { + const iconRgbColor = computeRgbColor(iconColor); + iconStyle["--icon-color"] = `rgb(${iconRgbColor})`; + iconStyle["--shape-color"] = `rgba(${iconRgbColor}, 0.2)`; } - - protected render() { - if (!this._config || !this.hass) { - return nothing; - } - - const icon = this.getValue("icon"); - const iconColor = this.getValue("icon_color"); - const badgeIcon = this.getValue("badge_icon"); - const badgeColor = this.getValue("badge_color"); - const primary = this.getValue("primary"); - const secondary = this.getValue("secondary"); - const picture = this.getValue("picture"); - - const multiline_secondary = this._config.multiline_secondary; - - const rtl = computeRTL(this.hass); - - const appearance = computeAppearance({ - fill_container: this._config.fill_container, - layout: this._config.layout, - icon_type: Boolean(picture) ? "entity-picture" : Boolean(icon) ? "icon" : "none", - primary_info: Boolean(primary) ? "name" : "none", - secondary_info: Boolean(secondary) ? "state" : "none", - }); - - const weatherSvg = getWeatherSvgIcon(icon); - - return html` - - - - ${picture - ? this.renderPicture(picture) - : weatherSvg - ? html`
${weatherSvg}
` - : icon - ? this.renderIcon(icon, iconColor) - : nothing} - ${(icon || picture) && badgeIcon - ? this.renderBadgeIcon(badgeIcon, badgeColor) - : undefined} - -
-
-
- `; + return html` + + + + `; + } + + renderBadgeIcon(badge: string, badgeColor?: string) { + const badgeStyle = {}; + if (badgeColor) { + const iconRgbColor = computeRgbColor(badgeColor); + badgeStyle["--main-color"] = `rgba(${iconRgbColor})`; } - - renderPicture(picture: string): TemplateResult { - return html` - - `; + return html` + + `; + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if (!this._config || !this.hass) { + return; } - renderIcon(icon: string, iconColor?: string) { - const iconStyle = {}; - if (iconColor) { - const iconRgbColor = computeRgbColor(iconColor); - iconStyle["--icon-color"] = `rgb(${iconRgbColor})`; - iconStyle["--shape-color"] = `rgba(${iconRgbColor}, 0.2)`; - } - return html` - - - - `; + this._tryConnect(); + } + + private async _tryConnect(): Promise { + TEMPLATE_KEYS.forEach((key) => { + this._tryConnectKey(key); + }); + } + + private async _tryConnectKey(key: TemplateKey): Promise { + if ( + this._unsubRenderTemplates.get(key) !== undefined || + !this.hass || + !this._config || + !this.isTemplate(key) + ) { + return; } - renderBadgeIcon(badge: string, badgeColor?: string) { - const badgeStyle = {}; - if (badgeColor) { - const iconRgbColor = computeRgbColor(badgeColor); - badgeStyle["--main-color"] = `rgba(${iconRgbColor})`; + try { + const sub = subscribeRenderTemplate( + this.hass.connection, + (result) => { + this._templateResults = { + ...this._templateResults, + [key]: result, + }; + }, + { + template: this._config[key] ?? "", + entity_ids: this._config.entity_id, + variables: { + config: this._config, + user: this.hass.user!.name, + entity: this._config.entity, + }, + strict: true, } - return html` - - `; + ); + this._unsubRenderTemplates.set(key, sub); + await sub; + } catch (_err) { + const result = { + result: this._config[key] ?? "", + listeners: { + all: false, + domains: [], + entities: [], + time: false, + }, + }; + this._templateResults = { + ...this._templateResults, + [key]: result, + }; + this._unsubRenderTemplates.delete(key); } - - protected updated(changedProps: PropertyValues): void { - super.updated(changedProps); - if (!this._config || !this.hass) { - return; - } - - this._tryConnect(); + } + private async _tryDisconnect(): Promise { + TEMPLATE_KEYS.forEach((key) => { + this._tryDisconnectKey(key); + }); + } + + private async _tryDisconnectKey(key: TemplateKey): Promise { + const unsubRenderTemplate = this._unsubRenderTemplates.get(key); + if (!unsubRenderTemplate) { + return; } - private async _tryConnect(): Promise { - TEMPLATE_KEYS.forEach((key) => { - this._tryConnectKey(key); - }); + try { + const unsub = await unsubRenderTemplate; + unsub(); + this._unsubRenderTemplates.delete(key); + } catch (err: any) { + if (err.code === "not_found" || err.code === "template_error") { + // If we get here, the connection was probably already closed. Ignore. + } else { + throw err; + } } - - private async _tryConnectKey(key: TemplateKey): Promise { - if ( - this._unsubRenderTemplates.get(key) !== undefined || - !this.hass || - !this._config || - !this.isTemplate(key) - ) { - return; - } - - try { - const sub = subscribeRenderTemplate( - this.hass.connection, - (result) => { - this._templateResults = { - ...this._templateResults, - [key]: result, - }; - }, - { - template: this._config[key] ?? "", - entity_ids: this._config.entity_id, - variables: { - config: this._config, - user: this.hass.user!.name, - entity: this._config.entity, - }, - strict: true, - } - ); - this._unsubRenderTemplates.set(key, sub); - await sub; - } catch (_err) { - const result = { - result: this._config[key] ?? "", - listeners: { - all: false, - domains: [], - entities: [], - time: false, - }, - }; - this._templateResults = { - ...this._templateResults, - [key]: result, - }; - this._unsubRenderTemplates.delete(key); + } + + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + mushroom-state-item { + cursor: pointer; } - } - private async _tryDisconnect(): Promise { - TEMPLATE_KEYS.forEach((key) => { - this._tryDisconnectKey(key); - }); - } - - private async _tryDisconnectKey(key: TemplateKey): Promise { - const unsubRenderTemplate = this._unsubRenderTemplates.get(key); - if (!unsubRenderTemplate) { - return; + mushroom-shape-icon { + --icon-color: rgb(var(--rgb-disabled)); + --shape-color: rgba(var(--rgb-disabled), 0.2); } - - try { - const unsub = await unsubRenderTemplate; - unsub(); - this._unsubRenderTemplates.delete(key); - } catch (err: any) { - if (err.code === "not_found" || err.code === "template_error") { - // If we get here, the connection was probably already closed. Ignore. - } else { - throw err; - } + svg { + width: var(--icon-size); + height: var(--icon-size); + display: flex; } - } - - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - mushroom-state-item { - cursor: pointer; - } - mushroom-shape-icon { - --icon-color: rgb(var(--rgb-disabled)); - --shape-color: rgba(var(--rgb-disabled), 0.2); - } - svg { - width: var(--icon-size); - height: var(--icon-size); - display: flex; - } - ${weatherSVGStyles} - `, - ]; - } + ${weatherSVGStyles} + `, + ]; + } } diff --git a/src/cards/title-card/title-card-config.ts b/src/cards/title-card/title-card-config.ts index 67efe64d7..7139f9f13 100644 --- a/src/cards/title-card/title-card-config.ts +++ b/src/cards/title-card/title-card-config.ts @@ -3,20 +3,20 @@ import { ActionConfig, actionConfigStruct, LovelaceCardConfig } from "../../ha"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; export interface TitleCardConfig extends LovelaceCardConfig { - title?: string; - subtitle?: string; - alignment?: string; - title_tap_action?: ActionConfig; - subtitle_tap_action?: ActionConfig; + title?: string; + subtitle?: string; + alignment?: string; + title_tap_action?: ActionConfig; + subtitle_tap_action?: ActionConfig; } export const titleCardConfigStruct = assign( - lovelaceCardConfigStruct, - object({ - title: optional(string()), - subtitle: optional(string()), - alignment: optional(string()), - title_tap_action: optional(actionConfigStruct), - subtitle_tap_action: optional(actionConfigStruct), - }) + lovelaceCardConfigStruct, + object({ + title: optional(string()), + subtitle: optional(string()), + alignment: optional(string()), + title_tap_action: optional(actionConfigStruct), + subtitle_tap_action: optional(actionConfigStruct), + }) ); diff --git a/src/cards/title-card/title-card-editor.ts b/src/cards/title-card/title-card-editor.ts index de7257ffa..30329a337 100644 --- a/src/cards/title-card/title-card-editor.ts +++ b/src/cards/title-card/title-card-editor.ts @@ -11,68 +11,78 @@ import { TITLE_CARD_EDITOR_NAME } from "./const"; import { TitleCardConfig, titleCardConfigStruct } from "./title-card-config"; const actions: UiAction[] = ["navigate", "url", "call-service", "none"]; -const TITLE_LABELS = ["title", "subtitle", "title_tap_action", "subtitle_tap_action"]; +const TITLE_LABELS = [ + "title", + "subtitle", + "title_tap_action", + "subtitle_tap_action", +]; const SCHEMA: HaFormSchema[] = [ - { - name: "title", - selector: { template: {} }, - }, - { - name: "subtitle", - selector: { template: {} }, - }, - { name: "alignment", selector: { mush_alignment: {} } }, - { - name: "title_tap_action", - selector: { "ui-action": { actions } }, - }, - { - name: "subtitle_tap_action", - selector: { "ui-action": { actions } }, - }, + { + name: "title", + selector: { template: {} }, + }, + { + name: "subtitle", + selector: { template: {} }, + }, + { name: "alignment", selector: { mush_alignment: {} } }, + { + name: "title_tap_action", + selector: { "ui-action": { actions } }, + }, + { + name: "subtitle_tap_action", + selector: { "ui-action": { actions } }, + }, ]; @customElement(TITLE_CARD_EDITOR_NAME) -export class TitleCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: TitleCardConfig; - - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } +export class TitleCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: TitleCardConfig; - public setConfig(config: TitleCardConfig): void { - assert(config, titleCardConfigStruct); - this._config = config; - } + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + public setConfig(config: TitleCardConfig): void { + assert(config, titleCardConfigStruct); + this._config = config; + } - if (TITLE_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.title.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - protected render() { - if (!this.hass || !this._config) { - return nothing; - } - - return html` - - `; + if (TITLE_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.title.${schema.name}`); } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/title-card/title-card.ts b/src/cards/title-card/title-card.ts index e607e15e0..5b2d773e3 100644 --- a/src/cards/title-card/title-card.ts +++ b/src/cards/title-card/title-card.ts @@ -4,15 +4,15 @@ import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { - actionHandler, - ActionHandlerEvent, - computeRTL, - handleAction, - HomeAssistant, - LovelaceCard, - LovelaceCardEditor, - RenderTemplateResult, - subscribeRenderTemplate, + actionHandler, + ActionHandlerEvent, + computeRTL, + handleAction, + HomeAssistant, + LovelaceCard, + LovelaceCardEditor, + RenderTemplateResult, + subscribeRenderTemplate, } from "../../ha"; import "../../shared/shape-icon"; import "../../shared/state-info"; @@ -24,9 +24,9 @@ import { TITLE_CARD_EDITOR_NAME, TITLE_CARD_NAME } from "./const"; import { TitleCardConfig } from "./title-card-config"; registerCustomCard({ - type: TITLE_CARD_NAME, - name: "Mushroom Title Card", - description: "Title and subtitle to separate sections", + type: TITLE_CARD_NAME, + name: "Mushroom Title Card", + description: "Title and subtitle to separate sections", }); const TEMPLATE_KEYS = ["title", "subtitle"] as const; @@ -34,300 +34,309 @@ type TemplateKey = (typeof TEMPLATE_KEYS)[number]; @customElement(TITLE_CARD_NAME) export class TitleCard extends MushroomBaseElement implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./title-card-editor"); - return document.createElement(TITLE_CARD_EDITOR_NAME) as LovelaceCardEditor; - } + public static async getConfigElement(): Promise { + await import("./title-card-editor"); + return document.createElement(TITLE_CARD_EDITOR_NAME) as LovelaceCardEditor; + } - public static async getStubConfig(_hass: HomeAssistant): Promise { - return { - type: `custom:${TITLE_CARD_NAME}`, - title: "Hello, {{ user }} !", - }; - } + public static async getStubConfig( + _hass: HomeAssistant + ): Promise { + return { + type: `custom:${TITLE_CARD_NAME}`, + title: "Hello, {{ user }} !", + }; + } - @state() private _config?: TitleCardConfig; + @state() private _config?: TitleCardConfig; - @state() private _templateResults: Partial< - Record - > = {}; + @state() private _templateResults: Partial< + Record + > = {}; - @state() private _unsubRenderTemplates: Map> = new Map(); + @state() private _unsubRenderTemplates: Map< + TemplateKey, + Promise + > = new Map(); - getCardSize(): number | Promise { - return 1; - } + getCardSize(): number | Promise { + return 1; + } - setConfig(config: TitleCardConfig): void { - TEMPLATE_KEYS.forEach((key) => { - if (this._config?.[key] !== config[key]) { - this._tryDisconnectKey(key); - } - }); - this._config = { - title_tap_action: { - action: "none", - }, - subtitle_tap_action: { - action: "none", - }, - ...config, - }; - } + setConfig(config: TitleCardConfig): void { + TEMPLATE_KEYS.forEach((key) => { + if (this._config?.[key] !== config[key]) { + this._tryDisconnectKey(key); + } + }); + this._config = { + title_tap_action: { + action: "none", + }, + subtitle_tap_action: { + action: "none", + }, + ...config, + }; + } - public connectedCallback() { - super.connectedCallback(); - this._tryConnect(); - } + public connectedCallback() { + super.connectedCallback(); + this._tryConnect(); + } - public disconnectedCallback() { - this._tryDisconnect(); - } + public disconnectedCallback() { + this._tryDisconnect(); + } - public isTemplate(key: TemplateKey) { - const value = this._config?.[key]; - return value?.includes("{"); - } + public isTemplate(key: TemplateKey) { + const value = this._config?.[key]; + return value?.includes("{"); + } - private getValue(key: TemplateKey) { - return this.isTemplate(key) - ? this._templateResults[key]?.result?.toString() - : this._config?.[key]; - } + private getValue(key: TemplateKey) { + return this.isTemplate(key) + ? this._templateResults[key]?.result?.toString() + : this._config?.[key]; + } + + private _handleTitleAction(ev: ActionHandlerEvent) { + const config = { + tap_action: this._config!.title_tap_action, + }; + handleAction(this, this.hass!, config, ev.detail.action!); + } - private _handleTitleAction(ev: ActionHandlerEvent) { - const config = { - tap_action: this._config!.title_tap_action, - }; - handleAction(this, this.hass!, config, ev.detail.action!); + private _handleSubtitleAction(ev: ActionHandlerEvent) { + const config = { + tap_action: this._config!.subtitle_tap_action, + }; + handleAction(this, this.hass!, config, ev.detail.action!); + } + + protected render() { + if (!this._config || !this.hass) { + return nothing; } - private _handleSubtitleAction(ev: ActionHandlerEvent) { - const config = { - tap_action: this._config!.subtitle_tap_action, - }; - handleAction(this, this.hass!, config, ev.detail.action!); + const title = this.getValue("title"); + const subtitle = this.getValue("subtitle"); + let alignment = ""; + if (this._config.alignment) { + alignment = `align-${this._config.alignment}`; } - protected render() { - if (!this._config || !this.hass) { - return nothing; - } + const actionableTitle = Boolean( + this._config.title_tap_action && + this._config.title_tap_action.action !== "none" + ); + const actionableSubtitle = Boolean( + this._config.subtitle_tap_action && + this._config.subtitle_tap_action.action !== "none" + ); - const title = this.getValue("title"); - const subtitle = this.getValue("subtitle"); - let alignment = ""; - if (this._config.alignment) { - alignment = `align-${this._config.alignment}`; - } + const rtl = computeRTL(this.hass); - const actionableTitle = Boolean( - this._config.title_tap_action && this._config.title_tap_action.action !== "none" - ); - const actionableSubtitle = Boolean( - this._config.subtitle_tap_action && this._config.subtitle_tap_action.action !== "none" - ); + return html` + + ${title + ? html` +
+

${title}${this.renderArrow()}

+
+ ` + : nothing} + ${subtitle + ? html` +
+

${subtitle}${this.renderArrow()}

+
+ ` + : nothing} +
+ `; + } - const rtl = computeRTL(this.hass); + private renderArrow() { + const rtl = computeRTL(this.hass); + return html` `; + } - return html` - - ${title - ? html` -
-

${title}${this.renderArrow()}

-
- ` - : nothing} - ${subtitle - ? html` -
-

${subtitle}${this.renderArrow()}

-
- ` - : nothing} -
- `; + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if (!this._config || !this.hass) { + return; } - private renderArrow() { - const rtl = computeRTL(this.hass); - return html` `; + this._tryConnect(); + } + + private async _tryConnect(): Promise { + TEMPLATE_KEYS.forEach((key) => { + this._tryConnectKey(key); + }); + } + + private async _tryConnectKey(key: TemplateKey): Promise { + if ( + this._unsubRenderTemplates.get(key) !== undefined || + !this.hass || + !this._config || + !this.isTemplate(key) + ) { + return; } - protected updated(changedProps: PropertyValues): void { - super.updated(changedProps); - if (!this._config || !this.hass) { - return; + try { + const sub = subscribeRenderTemplate( + this.hass.connection, + (result) => { + this._templateResults = { + ...this._templateResults, + [key]: result, + }; + }, + { + template: this._config[key] ?? "", + entity_ids: this._config.entity_id, + variables: { + config: this._config, + user: this.hass.user!.name, + }, + strict: true, } + ); + this._unsubRenderTemplates.set(key, sub); + await sub; + } catch (_err) { + const result = { + result: this._config[key] ?? "", + listeners: { + all: false, + domains: [], + entities: [], + time: false, + }, + }; + this._templateResults = { + ...this._templateResults, + [key]: result, + }; + this._unsubRenderTemplates.delete(key); + } + } + private async _tryDisconnect(): Promise { + TEMPLATE_KEYS.forEach((key) => { + this._tryDisconnectKey(key); + }); + } - this._tryConnect(); + private async _tryDisconnectKey(key: TemplateKey): Promise { + const unsubRenderTemplate = this._unsubRenderTemplates.get(key); + if (!unsubRenderTemplate) { + return; } - private async _tryConnect(): Promise { - TEMPLATE_KEYS.forEach((key) => { - this._tryConnectKey(key); - }); + try { + const unsub = await unsubRenderTemplate; + unsub(); + this._unsubRenderTemplates.delete(key); + } catch (err: any) { + if (err.code === "not_found" || err.code === "template_error") { + // If we get here, the connection was probably already closed. Ignore. + } else { + throw err; + } } + } - private async _tryConnectKey(key: TemplateKey): Promise { - if ( - this._unsubRenderTemplates.get(key) !== undefined || - !this.hass || - !this._config || - !this.isTemplate(key) - ) { - return; + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + .header { + display: block; + padding: var(--title-padding); + background: none; + border: none; + box-shadow: none; } - - try { - const sub = subscribeRenderTemplate( - this.hass.connection, - (result) => { - this._templateResults = { - ...this._templateResults, - [key]: result, - }; - }, - { - template: this._config[key] ?? "", - entity_ids: this._config.entity_id, - variables: { - config: this._config, - user: this.hass.user!.name, - }, - strict: true, - } - ); - this._unsubRenderTemplates.set(key, sub); - await sub; - } catch (_err) { - const result = { - result: this._config[key] ?? "", - listeners: { - all: false, - domains: [], - entities: [], - time: false, - }, - }; - this._templateResults = { - ...this._templateResults, - [key]: result, - }; - this._unsubRenderTemplates.delete(key); + .header div * { + margin: 0; + white-space: pre-wrap; } - } - private async _tryDisconnect(): Promise { - TEMPLATE_KEYS.forEach((key) => { - this._tryDisconnectKey(key); - }); - } - - private async _tryDisconnectKey(key: TemplateKey): Promise { - const unsubRenderTemplate = this._unsubRenderTemplates.get(key); - if (!unsubRenderTemplate) { - return; + .header div:not(:last-of-type) { + margin-bottom: var(--title-spacing); } - - try { - const unsub = await unsubRenderTemplate; - unsub(); - this._unsubRenderTemplates.delete(key); - } catch (err: any) { - if (err.code === "not_found" || err.code === "template_error") { - // If we get here, the connection was probably already closed. Ignore. - } else { - throw err; - } + .actionable { + cursor: pointer; } - } - - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - .header { - display: block; - padding: var(--title-padding); - background: none; - border: none; - box-shadow: none; - } - .header div * { - margin: 0; - white-space: pre-wrap; - } - .header div:not(:last-of-type) { - margin-bottom: var(--title-spacing); - } - .actionable { - cursor: pointer; - } - .header ha-icon { - display: none; - } - .actionable ha-icon { - display: inline-block; - margin-left: 4px; - transition: transform 180ms ease-in-out; - } - .actionable:hover ha-icon { - transform: translateX(4px); - } - [rtl] .actionable ha-icon { - margin-left: initial; - margin-right: 4px; - } - [rtl] .actionable:hover ha-icon { - transform: translateX(-4px); - } - .title { - color: var(--title-color); - font-size: var(--title-font-size); - font-weight: var(--title-font-weight); - line-height: var(--title-line-height); - letter-spacing: var(--title-letter-spacing); - --mdc-icon-size: var(--title-font-size); - } - .subtitle { - color: var(--subtitle-color); - font-size: var(--subtitle-font-size); - font-weight: var(--subtitle-font-weight); - line-height: var(--subtitle-line-height); - letter-spacing: var(--subtitle-letter-spacing); - --mdc-icon-size: var(--subtitle-font-size); - } - .align-start { - text-align: start; - } - .align-end { - text-align: end; - } - .align-center { - text-align: center; - } - .align-justify { - text-align: justify; - } - `, - ]; - } + .header ha-icon { + display: none; + } + .actionable ha-icon { + display: inline-block; + margin-left: 4px; + transition: transform 180ms ease-in-out; + } + .actionable:hover ha-icon { + transform: translateX(4px); + } + [rtl] .actionable ha-icon { + margin-left: initial; + margin-right: 4px; + } + [rtl] .actionable:hover ha-icon { + transform: translateX(-4px); + } + .title { + color: var(--title-color); + font-size: var(--title-font-size); + font-weight: var(--title-font-weight); + line-height: var(--title-line-height); + letter-spacing: var(--title-letter-spacing); + --mdc-icon-size: var(--title-font-size); + } + .subtitle { + color: var(--subtitle-color); + font-size: var(--subtitle-font-size); + font-weight: var(--subtitle-font-weight); + line-height: var(--subtitle-line-height); + letter-spacing: var(--subtitle-letter-spacing); + --mdc-icon-size: var(--subtitle-font-size); + } + .align-start { + text-align: start; + } + .align-end { + text-align: end; + } + .align-center { + text-align: center; + } + .align-justify { + text-align: justify; + } + `, + ]; + } } diff --git a/src/cards/update-card/const.ts b/src/cards/update-card/const.ts index 9ccae38bc..b21594bd1 100644 --- a/src/cards/update-card/const.ts +++ b/src/cards/update-card/const.ts @@ -7,7 +7,7 @@ export const UPDATE_ENTITY_DOMAINS = ["update"]; export const UPDATE_CARD_DEFAULT_STATE_COLOR = "var(--rgb-grey)"; export const UPDATE_CARD_STATE_COLOR = { - on: "var(--rgb-state-update-on)", - off: "var(--rgb-state-update-off)", - installing: "var(--rgb-state-update-installing)", + on: "var(--rgb-state-update-on)", + off: "var(--rgb-state-update-off)", + installing: "var(--rgb-state-update-installing)", }; diff --git a/src/cards/update-card/controls/update-buttons-control.ts b/src/cards/update-card/controls/update-buttons-control.ts index bf7665fac..fb255c624 100644 --- a/src/cards/update-card/controls/update-buttons-control.ts +++ b/src/cards/update-card/controls/update-buttons-control.ts @@ -1,65 +1,80 @@ import { html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; import { - computeRTL, - HomeAssistant, - isActive, - isAvailable, - UpdateEntity, - updateIsInstalling, + computeRTL, + HomeAssistant, + isActive, + isAvailable, + UpdateEntity, + updateIsInstalling, } from "../../../ha"; import "../../../shared/button"; import "../../../shared/button-group"; @customElement("mushroom-update-buttons-control") export class UpdateButtonsControl extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public entity!: UpdateEntity; + @property({ attribute: false }) public entity!: UpdateEntity; - @property({ type: Boolean }) public fill: boolean = false; + @property({ type: Boolean }) public fill: boolean = false; - private _handleInstall(): void { - this.hass.callService("update", "install", { - entity_id: this.entity.entity_id, - }); - } + private _handleInstall(): void { + this.hass.callService("update", "install", { + entity_id: this.entity.entity_id, + }); + } - private _handleSkip(e: MouseEvent): void { - e.stopPropagation(); - this.hass.callService("update", "skip", { - entity_id: this.entity.entity_id, - }); - } + private _handleSkip(e: MouseEvent): void { + e.stopPropagation(); + this.hass.callService("update", "skip", { + entity_id: this.entity.entity_id, + }); + } - private get installDisabled(): boolean { - if (!isAvailable(this.entity)) return true; - const skippedVersion = - this.entity.attributes.latest_version && - this.entity.attributes.skipped_version === this.entity.attributes.latest_version; - return (!isActive(this.entity) && !skippedVersion) || updateIsInstalling(this.entity); - } + private get installDisabled(): boolean { + if (!isAvailable(this.entity)) return true; + const skippedVersion = + this.entity.attributes.latest_version && + this.entity.attributes.skipped_version === + this.entity.attributes.latest_version; + return ( + (!isActive(this.entity) && !skippedVersion) || + updateIsInstalling(this.entity) + ); + } - private get skipDisabled(): boolean { - if (!isAvailable(this.entity)) return true; - const skippedVersion = - this.entity.attributes.latest_version && - this.entity.attributes.skipped_version === this.entity.attributes.latest_version; - return skippedVersion || !isActive(this.entity) || updateIsInstalling(this.entity); - } + private get skipDisabled(): boolean { + if (!isAvailable(this.entity)) return true; + const skippedVersion = + this.entity.attributes.latest_version && + this.entity.attributes.skipped_version === + this.entity.attributes.latest_version; + return ( + skippedVersion || + !isActive(this.entity) || + updateIsInstalling(this.entity) + ); + } - protected render(): TemplateResult { - const rtl = computeRTL(this.hass); + protected render(): TemplateResult { + const rtl = computeRTL(this.hass); - return html` - - - - - - - - - `; - } + return html` + + + + + + + + + `; + } } diff --git a/src/cards/update-card/update-card-config.ts b/src/cards/update-card/update-card-config.ts index fe051598d..bec3597ae 100644 --- a/src/cards/update-card/update-card-config.ts +++ b/src/cards/update-card/update-card-config.ts @@ -1,26 +1,36 @@ import { assign, boolean, object, optional } from "superstruct"; import { LovelaceCardConfig } from "../../ha"; -import { ActionsSharedConfig, actionsSharedConfigStruct } from "../../shared/config/actions-config"; import { - AppearanceSharedConfig, - appearanceSharedConfigStruct, + ActionsSharedConfig, + actionsSharedConfigStruct, +} from "../../shared/config/actions-config"; +import { + AppearanceSharedConfig, + appearanceSharedConfigStruct, } from "../../shared/config/appearance-config"; -import { EntitySharedConfig, entitySharedConfigStruct } from "../../shared/config/entity-config"; +import { + EntitySharedConfig, + entitySharedConfigStruct, +} from "../../shared/config/entity-config"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; export type UpdateCardConfig = LovelaceCardConfig & - EntitySharedConfig & - AppearanceSharedConfig & - ActionsSharedConfig & { - show_buttons_control?: boolean; - collapsible_controls?: boolean; - }; + EntitySharedConfig & + AppearanceSharedConfig & + ActionsSharedConfig & { + show_buttons_control?: boolean; + collapsible_controls?: boolean; + }; export const updateCardConfigStruct = assign( - lovelaceCardConfigStruct, - assign(entitySharedConfigStruct, appearanceSharedConfigStruct, actionsSharedConfigStruct), - object({ - show_buttons_control: optional(boolean()), - collapsible_controls: optional(boolean()), - }) + lovelaceCardConfigStruct, + assign( + entitySharedConfigStruct, + appearanceSharedConfigStruct, + actionsSharedConfigStruct + ), + object({ + show_buttons_control: optional(boolean()), + collapsible_controls: optional(boolean()), + }) ); diff --git a/src/cards/update-card/update-card-editor.ts b/src/cards/update-card/update-card-editor.ts index 535f064bb..f1ddaf7ac 100644 --- a/src/cards/update-card/update-card-editor.ts +++ b/src/cards/update-card/update-card-editor.ts @@ -15,67 +15,79 @@ import { UpdateCardConfig, updateCardConfigStruct } from "./update-card-config"; const UPDATE_LABELS = ["show_buttons_control"]; -const actions: UiAction[] = ["more-info", "navigate", "url", "call-service", "assist", "none"]; +const actions: UiAction[] = [ + "more-info", + "navigate", + "url", + "call-service", + "assist", + "none", +]; const SCHEMA: HaFormSchema[] = [ - { name: "entity", selector: { entity: { domain: UPDATE_ENTITY_DOMAINS } } }, - { name: "name", selector: { text: {} } }, - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - ...APPEARANCE_FORM_SCHEMA, - { - type: "grid", - name: "", - schema: [ - { name: "show_buttons_control", selector: { boolean: {} } }, - { name: "collapsible_controls", selector: { boolean: {} } }, - ], - }, - ...computeActionsFormSchema(actions), + { name: "entity", selector: { entity: { domain: UPDATE_ENTITY_DOMAINS } } }, + { name: "name", selector: { text: {} } }, + { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, + ...APPEARANCE_FORM_SCHEMA, + { + type: "grid", + name: "", + schema: [ + { name: "show_buttons_control", selector: { boolean: {} } }, + { name: "collapsible_controls", selector: { boolean: {} } }, + ], + }, + ...computeActionsFormSchema(actions), ]; @customElement(UPDATE_CARD_EDITOR_NAME) -export class UpdateCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: UpdateCardConfig; - - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } - - public setConfig(config: UpdateCardConfig): void { - assert(config, updateCardConfigStruct); - this._config = config; - } +export class UpdateCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: UpdateCardConfig; - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - if (UPDATE_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.update.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + public setConfig(config: UpdateCardConfig): void { + assert(config, updateCardConfigStruct); + this._config = config; + } - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - return html` - - `; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); + } + if (UPDATE_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.update.${schema.name}`); } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); + protected render() { + if (!this.hass || !this._config) { + return nothing; } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/update-card/update-card.ts b/src/cards/update-card/update-card.ts index bc2cb89c4..2f7e8cb26 100644 --- a/src/cards/update-card/update-card.ts +++ b/src/cards/update-card/update-card.ts @@ -3,20 +3,20 @@ import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - computeRTL, - handleAction, - hasAction, - HomeAssistant, - isActive, - isAvailable, - LovelaceCard, - LovelaceCardEditor, - supportsFeature, - UPDATE_SUPPORT_INSTALL, - UpdateEntity, - updateIsInstalling, + actionHandler, + ActionHandlerEvent, + computeRTL, + handleAction, + hasAction, + HomeAssistant, + isActive, + isAvailable, + LovelaceCard, + LovelaceCardEditor, + supportsFeature, + UPDATE_SUPPORT_INSTALL, + UpdateEntity, + updateIsInstalling, } from "../../ha"; import "../../shared/badge-icon"; import "../../shared/card"; @@ -28,150 +28,164 @@ import { MushroomBaseCard } from "../../utils/base-card"; import { cardStyle } from "../../utils/card-styles"; import { registerCustomCard } from "../../utils/custom-cards"; import { computeEntityPicture } from "../../utils/info"; -import { UPDATE_CARD_EDITOR_NAME, UPDATE_CARD_NAME, UPDATE_ENTITY_DOMAINS } from "./const"; +import { + UPDATE_CARD_EDITOR_NAME, + UPDATE_CARD_NAME, + UPDATE_ENTITY_DOMAINS, +} from "./const"; import "./controls/update-buttons-control"; import { UpdateCardConfig } from "./update-card-config"; import { getStateColor } from "./utils"; registerCustomCard({ - type: UPDATE_CARD_NAME, - name: "Mushroom Update Card", - description: "Card for update entity", + type: UPDATE_CARD_NAME, + name: "Mushroom Update Card", + description: "Card for update entity", }); @customElement(UPDATE_CARD_NAME) export class UpdateCard - extends MushroomBaseCard - implements LovelaceCard + extends MushroomBaseCard + implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./update-card-editor"); - return document.createElement(UPDATE_CARD_EDITOR_NAME) as LovelaceCardEditor; - } - - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - const updates = entities.filter((e) => UPDATE_ENTITY_DOMAINS.includes(e.split(".")[0])); - return { - type: `custom:${UPDATE_CARD_NAME}`, - entity: updates[0], - }; + public static async getConfigElement(): Promise { + await import("./update-card-editor"); + return document.createElement( + UPDATE_CARD_EDITOR_NAME + ) as LovelaceCardEditor; + } + + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + const updates = entities.filter((e) => + UPDATE_ENTITY_DOMAINS.includes(e.split(".")[0]) + ); + return { + type: `custom:${UPDATE_CARD_NAME}`, + entity: updates[0], + }; + } + + protected get hasControls() { + if (!this._stateObj || !this._config) return false; + return ( + Boolean(this._config.show_buttons_control) && + supportsFeature(this._stateObj, UPDATE_SUPPORT_INSTALL) + ); + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + protected render() { + if (!this._config || !this.hass || !this._config.entity) { + return nothing; } - protected get hasControls() { - if (!this._stateObj || !this._config) return false; - return ( - Boolean(this._config.show_buttons_control) && - supportsFeature(this._stateObj, UPDATE_SUPPORT_INSTALL) - ); - } + const stateObj = this._stateObj; - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); + if (!stateObj) { + return this.renderNotFound(this._config); } - protected render() { - if (!this._config || !this.hass || !this._config.entity) { - return nothing; + const name = this._config.name || stateObj.attributes.friendly_name || ""; + const icon = this._config.icon; + const appearance = computeAppearance(this._config); + const picture = computeEntityPicture(stateObj, appearance.icon_type); + + const rtl = computeRTL(this.hass); + + const displayControls = + (!this._config.collapsible_controls || isActive(stateObj)) && + this._config.show_buttons_control && + supportsFeature(stateObj, UPDATE_SUPPORT_INSTALL); + + return html` + + + + ${picture + ? this.renderPicture(picture) + : this.renderIcon(stateObj, icon)} + ${this.renderBadge(stateObj)} + ${this.renderStateInfo(stateObj, appearance, name)}; + + ${displayControls + ? html` +
+ +
+ ` + : nothing} +
+
+ `; + } + + protected renderIcon(stateObj: UpdateEntity, icon?: string): TemplateResult { + const isInstalling = updateIsInstalling(stateObj); + + const color = getStateColor(stateObj.state, isInstalling); + + const style = { + "--icon-color": `rgb(${color})`, + "--shape-color": `rgba(${color}, 0.2)`, + }; + + return html` + + + + `; + } + + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + mushroom-state-item { + cursor: pointer; } - - const stateObj = this._stateObj; - - if (!stateObj) { - return this.renderNotFound(this._config); + mushroom-shape-icon { + --icon-color: rgb(var(--rgb-state-entity)); + --shape-color: rgba(var(--rgb-state-entity), 0.2); } - - const name = this._config.name || stateObj.attributes.friendly_name || ""; - const icon = this._config.icon; - const appearance = computeAppearance(this._config); - const picture = computeEntityPicture(stateObj, appearance.icon_type); - - const rtl = computeRTL(this.hass); - - const displayControls = - (!this._config.collapsible_controls || isActive(stateObj)) && - this._config.show_buttons_control && - supportsFeature(stateObj, UPDATE_SUPPORT_INSTALL); - - return html` - - - - ${picture ? this.renderPicture(picture) : this.renderIcon(stateObj, icon)} - ${this.renderBadge(stateObj)} - ${this.renderStateInfo(stateObj, appearance, name)}; - - ${displayControls - ? html` -
- -
- ` - : nothing} -
-
- `; - } - - protected renderIcon(stateObj: UpdateEntity, icon?: string): TemplateResult { - const isInstalling = updateIsInstalling(stateObj); - - const color = getStateColor(stateObj.state, isInstalling); - - const style = { - "--icon-color": `rgb(${color})`, - "--shape-color": `rgba(${color}, 0.2)`, - }; - - return html` - - - - `; - } - - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - mushroom-state-item { - cursor: pointer; - } - mushroom-shape-icon { - --icon-color: rgb(var(--rgb-state-entity)); - --shape-color: rgba(var(--rgb-state-entity), 0.2); - } - mushroom-shape-icon.pulse { - --shape-animation: 1s ease 0s infinite normal none running pulse; - } - mushroom-update-buttons-control { - flex: 1; - } - `, - ]; - } + mushroom-shape-icon.pulse { + --shape-animation: 1s ease 0s infinite normal none running pulse; + } + mushroom-update-buttons-control { + flex: 1; + } + `, + ]; + } } diff --git a/src/cards/update-card/utils.ts b/src/cards/update-card/utils.ts index 79c63108a..a9eeb8c93 100644 --- a/src/cards/update-card/utils.ts +++ b/src/cards/update-card/utils.ts @@ -1,9 +1,12 @@ -import { UPDATE_CARD_DEFAULT_STATE_COLOR, UPDATE_CARD_STATE_COLOR } from "./const"; +import { + UPDATE_CARD_DEFAULT_STATE_COLOR, + UPDATE_CARD_STATE_COLOR, +} from "./const"; export function getStateColor(state: string, isInstalling: boolean): string { - if (isInstalling) { - return UPDATE_CARD_STATE_COLOR["installing"]; - } else { - return UPDATE_CARD_STATE_COLOR[state] || UPDATE_CARD_DEFAULT_STATE_COLOR; - } + if (isInstalling) { + return UPDATE_CARD_STATE_COLOR["installing"]; + } else { + return UPDATE_CARD_STATE_COLOR[state] || UPDATE_CARD_DEFAULT_STATE_COLOR; + } } diff --git a/src/cards/vacuum-card/controls/vacuum-commands-control.ts b/src/cards/vacuum-card/controls/vacuum-commands-control.ts index f937aefd3..efd44ad64 100644 --- a/src/cards/vacuum-card/controls/vacuum-commands-control.ts +++ b/src/cards/vacuum-card/controls/vacuum-commands-control.ts @@ -1,164 +1,173 @@ import { html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; import { - computeRTL, - HomeAssistant, - isActive, - isAvailable, - supportsFeature, - VacuumEntity, - VACUUM_SUPPORT_CLEAN_SPOT, - VACUUM_SUPPORT_LOCATE, - VACUUM_SUPPORT_PAUSE, - VACUUM_SUPPORT_RETURN_HOME, - VACUUM_SUPPORT_START, - VACUUM_SUPPORT_STOP, - VACUUM_SUPPORT_TURN_OFF, - VACUUM_SUPPORT_TURN_ON, + computeRTL, + HomeAssistant, + isActive, + isAvailable, + supportsFeature, + VacuumEntity, + VACUUM_SUPPORT_CLEAN_SPOT, + VACUUM_SUPPORT_LOCATE, + VACUUM_SUPPORT_PAUSE, + VACUUM_SUPPORT_RETURN_HOME, + VACUUM_SUPPORT_START, + VACUUM_SUPPORT_STOP, + VACUUM_SUPPORT_TURN_OFF, + VACUUM_SUPPORT_TURN_ON, } from "../../../ha"; import { isCleaning, isReturningHome, isStopped } from "../utils"; import { VacuumCommand } from "../vacuum-card-config"; interface VacuumButton { - icon: string; - serviceName: string; - command: VacuumCommand; - isSupported: (entity: VacuumEntity) => boolean; - isVisible?: (entity: VacuumEntity) => boolean; - isDisabled: (entity: VacuumEntity) => boolean; + icon: string; + serviceName: string; + command: VacuumCommand; + isSupported: (entity: VacuumEntity) => boolean; + isVisible?: (entity: VacuumEntity) => boolean; + isDisabled: (entity: VacuumEntity) => boolean; } export const isButtonVisible = ( - entity: VacuumEntity, - button: VacuumButton, - commands: VacuumCommand[] -) => isButtonSupported(entity, button, commands) && (!button.isVisible || button.isVisible(entity)); + entity: VacuumEntity, + button: VacuumButton, + commands: VacuumCommand[] +) => + isButtonSupported(entity, button, commands) && + (!button.isVisible || button.isVisible(entity)); export const isButtonSupported = ( - entity: VacuumEntity, - button: VacuumButton, - commands: VacuumCommand[] + entity: VacuumEntity, + button: VacuumButton, + commands: VacuumCommand[] ) => button.isSupported(entity) && commands.includes(button.command); -export const isCommandsControlVisible = (entity: VacuumEntity, commands: VacuumCommand[]) => - VACUUM_BUTTONS.some((button) => isButtonVisible(entity, button, commands)); +export const isCommandsControlVisible = ( + entity: VacuumEntity, + commands: VacuumCommand[] +) => VACUUM_BUTTONS.some((button) => isButtonVisible(entity, button, commands)); -export const isCommandsControlSupported = (entity: VacuumEntity, commands: VacuumCommand[]) => - VACUUM_BUTTONS.some((button) => isButtonSupported(entity, button, commands)); +export const isCommandsControlSupported = ( + entity: VacuumEntity, + commands: VacuumCommand[] +) => + VACUUM_BUTTONS.some((button) => isButtonSupported(entity, button, commands)); export const VACUUM_BUTTONS: VacuumButton[] = [ - { - icon: "mdi:power", - serviceName: "turn_on", - command: "on_off", - isSupported: (entity) => supportsFeature(entity, VACUUM_SUPPORT_TURN_ON), - isVisible: (entity) => !isActive(entity), - isDisabled: () => false, - }, - { - icon: "mdi:power", - serviceName: "turn_off", - command: "on_off", - isSupported: (entity) => supportsFeature(entity, VACUUM_SUPPORT_TURN_OFF), - isVisible: (entity) => isActive(entity), - isDisabled: () => false, - }, - { - icon: "mdi:play", - serviceName: "start", - command: "start_pause", - isSupported: (entity) => supportsFeature(entity, VACUUM_SUPPORT_START), - isVisible: (entity) => !isCleaning(entity), - isDisabled: () => false, - }, - { - icon: "mdi:pause", - serviceName: "pause", - command: "start_pause", - isSupported: (entity) => - // We need also to check if Start is supported because if not we show play-pause - supportsFeature(entity, VACUUM_SUPPORT_START) && - supportsFeature(entity, VACUUM_SUPPORT_PAUSE), - isVisible: (entity) => isCleaning(entity), - isDisabled: () => false, - }, - { - icon: "mdi:play-pause", - serviceName: "start_pause", - command: "start_pause", - isSupported: (entity) => - // If start is supported, we don't show this button - !supportsFeature(entity, VACUUM_SUPPORT_START) && - supportsFeature(entity, VACUUM_SUPPORT_PAUSE), - isDisabled: () => false, - }, - { - icon: "mdi:stop", - serviceName: "stop", - command: "stop", - isSupported: (entity) => supportsFeature(entity, VACUUM_SUPPORT_STOP), - isDisabled: (entity) => isStopped(entity), - }, - { - icon: "mdi:target-variant", - serviceName: "clean_spot", - command: "clean_spot", - isSupported: (entity) => supportsFeature(entity, VACUUM_SUPPORT_CLEAN_SPOT), - isDisabled: () => false, - }, - { - icon: "mdi:map-marker", - serviceName: "locate", - command: "locate", - isSupported: (entity) => supportsFeature(entity, VACUUM_SUPPORT_LOCATE), - isDisabled: (entity) => isReturningHome(entity), - }, - { - icon: "mdi:home-map-marker", - serviceName: "return_to_base", - command: "return_home", - isSupported: (entity) => supportsFeature(entity, VACUUM_SUPPORT_RETURN_HOME), - isDisabled: () => false, - }, + { + icon: "mdi:power", + serviceName: "turn_on", + command: "on_off", + isSupported: (entity) => supportsFeature(entity, VACUUM_SUPPORT_TURN_ON), + isVisible: (entity) => !isActive(entity), + isDisabled: () => false, + }, + { + icon: "mdi:power", + serviceName: "turn_off", + command: "on_off", + isSupported: (entity) => supportsFeature(entity, VACUUM_SUPPORT_TURN_OFF), + isVisible: (entity) => isActive(entity), + isDisabled: () => false, + }, + { + icon: "mdi:play", + serviceName: "start", + command: "start_pause", + isSupported: (entity) => supportsFeature(entity, VACUUM_SUPPORT_START), + isVisible: (entity) => !isCleaning(entity), + isDisabled: () => false, + }, + { + icon: "mdi:pause", + serviceName: "pause", + command: "start_pause", + isSupported: (entity) => + // We need also to check if Start is supported because if not we show play-pause + supportsFeature(entity, VACUUM_SUPPORT_START) && + supportsFeature(entity, VACUUM_SUPPORT_PAUSE), + isVisible: (entity) => isCleaning(entity), + isDisabled: () => false, + }, + { + icon: "mdi:play-pause", + serviceName: "start_pause", + command: "start_pause", + isSupported: (entity) => + // If start is supported, we don't show this button + !supportsFeature(entity, VACUUM_SUPPORT_START) && + supportsFeature(entity, VACUUM_SUPPORT_PAUSE), + isDisabled: () => false, + }, + { + icon: "mdi:stop", + serviceName: "stop", + command: "stop", + isSupported: (entity) => supportsFeature(entity, VACUUM_SUPPORT_STOP), + isDisabled: (entity) => isStopped(entity), + }, + { + icon: "mdi:target-variant", + serviceName: "clean_spot", + command: "clean_spot", + isSupported: (entity) => supportsFeature(entity, VACUUM_SUPPORT_CLEAN_SPOT), + isDisabled: () => false, + }, + { + icon: "mdi:map-marker", + serviceName: "locate", + command: "locate", + isSupported: (entity) => supportsFeature(entity, VACUUM_SUPPORT_LOCATE), + isDisabled: (entity) => isReturningHome(entity), + }, + { + icon: "mdi:home-map-marker", + serviceName: "return_to_base", + command: "return_home", + isSupported: (entity) => + supportsFeature(entity, VACUUM_SUPPORT_RETURN_HOME), + isDisabled: () => false, + }, ]; @customElement("mushroom-vacuum-commands-control") export class CoverButtonsControl extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public entity!: VacuumEntity; + @property({ attribute: false }) public entity!: VacuumEntity; - @property({ attribute: false }) public commands!: VacuumCommand[]; + @property({ attribute: false }) public commands!: VacuumCommand[]; - @property({ type: Boolean }) public fill: boolean = false; + @property({ type: Boolean }) public fill: boolean = false; - private callService(e: CustomEvent) { - e.stopPropagation(); - const entry = (e.target! as any).entry as VacuumButton; - this.hass.callService("vacuum", entry.serviceName, { - entity_id: this.entity!.entity_id, - }); - } + private callService(e: CustomEvent) { + e.stopPropagation(); + const entry = (e.target! as any).entry as VacuumButton; + this.hass.callService("vacuum", entry.serviceName, { + entity_id: this.entity!.entity_id, + }); + } - protected render(): TemplateResult { - const rtl = computeRTL(this.hass); + protected render(): TemplateResult { + const rtl = computeRTL(this.hass); - return html` - - ${VACUUM_BUTTONS.filter((item) => - isButtonVisible(this.entity, item, this.commands) - ).map( - (item) => html` - - - - ` - )} - - `; - } + return html` + + ${VACUUM_BUTTONS.filter((item) => + isButtonVisible(this.entity, item, this.commands) + ).map( + (item) => html` + + + + ` + )} + + `; + } } diff --git a/src/cards/vacuum-card/utils.ts b/src/cards/vacuum-card/utils.ts index 0094ce4fe..2e5fd2d1f 100644 --- a/src/cards/vacuum-card/utils.ts +++ b/src/cards/vacuum-card/utils.ts @@ -1,40 +1,40 @@ import { HassEntity } from "home-assistant-js-websocket"; import { - STATE_CLEANING, - STATE_DOCKED, - STATE_IDLE, - STATE_OFF, - STATE_ON, - STATE_RETURNING, + STATE_CLEANING, + STATE_DOCKED, + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_RETURNING, } from "../../ha"; export function isCleaning(stateObj: HassEntity): boolean { - switch (stateObj.state) { - case STATE_CLEANING: - case STATE_ON: - return true; - default: - return false; - } + switch (stateObj.state) { + case STATE_CLEANING: + case STATE_ON: + return true; + default: + return false; + } } export function isStopped(stateObj: HassEntity): boolean { - switch (stateObj.state) { - case STATE_DOCKED: - case STATE_OFF: - case STATE_IDLE: - case STATE_RETURNING: - return true; - default: - return false; - } + switch (stateObj.state) { + case STATE_DOCKED: + case STATE_OFF: + case STATE_IDLE: + case STATE_RETURNING: + return true; + default: + return false; + } } export function isReturningHome(stateObj: HassEntity): boolean { - switch (stateObj.state) { - case STATE_RETURNING: - return true; - default: - return false; - } + switch (stateObj.state) { + case STATE_RETURNING: + return true; + default: + return false; + } } diff --git a/src/cards/vacuum-card/vacuum-card-config.ts b/src/cards/vacuum-card/vacuum-card-config.ts index 8749be7c3..e79be18cc 100644 --- a/src/cards/vacuum-card/vacuum-card-config.ts +++ b/src/cards/vacuum-card/vacuum-card-config.ts @@ -1,37 +1,47 @@ import { array, assign, boolean, object, optional, string } from "superstruct"; import { LovelaceCardConfig } from "../../ha"; -import { ActionsSharedConfig, actionsSharedConfigStruct } from "../../shared/config/actions-config"; import { - AppearanceSharedConfig, - appearanceSharedConfigStruct, + ActionsSharedConfig, + actionsSharedConfigStruct, +} from "../../shared/config/actions-config"; +import { + AppearanceSharedConfig, + appearanceSharedConfigStruct, } from "../../shared/config/appearance-config"; -import { EntitySharedConfig, entitySharedConfigStruct } from "../../shared/config/entity-config"; +import { + EntitySharedConfig, + entitySharedConfigStruct, +} from "../../shared/config/entity-config"; import { lovelaceCardConfigStruct } from "../../shared/config/lovelace-card-config"; export const VACUUM_COMMANDS = [ - "on_off", - "start_pause", - "stop", - "locate", - "clean_spot", - "return_home", + "on_off", + "start_pause", + "stop", + "locate", + "clean_spot", + "return_home", ] as const; export type VacuumCommand = (typeof VACUUM_COMMANDS)[number]; export type VacuumCardConfig = LovelaceCardConfig & - EntitySharedConfig & - AppearanceSharedConfig & - ActionsSharedConfig & { - icon_animation?: boolean; - commands?: VacuumCommand[]; - }; + EntitySharedConfig & + AppearanceSharedConfig & + ActionsSharedConfig & { + icon_animation?: boolean; + commands?: VacuumCommand[]; + }; export const vacuumCardConfigStruct = assign( - lovelaceCardConfigStruct, - assign(entitySharedConfigStruct, appearanceSharedConfigStruct, actionsSharedConfigStruct), - object({ - icon_animation: optional(boolean()), - commands: optional(array(string())), - }) + lovelaceCardConfigStruct, + assign( + entitySharedConfigStruct, + appearanceSharedConfigStruct, + actionsSharedConfigStruct + ), + object({ + icon_animation: optional(boolean()), + commands: optional(array(string())), + }) ); diff --git a/src/cards/vacuum-card/vacuum-card-editor.ts b/src/cards/vacuum-card/vacuum-card-editor.ts index da6284148..5e774f670 100644 --- a/src/cards/vacuum-card/vacuum-card-editor.ts +++ b/src/cards/vacuum-card/vacuum-card-editor.ts @@ -11,89 +11,102 @@ import { GENERIC_LABELS } from "../../utils/form/generic-fields"; import { HaFormSchema } from "../../utils/form/ha-form"; import { loadHaComponents } from "../../utils/loader"; import { VACUUM_CARD_EDITOR_NAME, VACUUM_ENTITY_DOMAINS } from "./const"; -import { VACUUM_COMMANDS, VacuumCardConfig, vacuumCardConfigStruct } from "./vacuum-card-config"; +import { + VACUUM_COMMANDS, + VacuumCardConfig, + vacuumCardConfigStruct, +} from "./vacuum-card-config"; const VACUUM_LABELS = ["commands"]; const computeSchema = memoizeOne( - (localize: LocalizeFunc, customLocalize: LocalizeFunc): HaFormSchema[] => [ - { name: "entity", selector: { entity: { domain: VACUUM_ENTITY_DOMAINS } } }, - { name: "name", selector: { text: {} } }, + (localize: LocalizeFunc, customLocalize: LocalizeFunc): HaFormSchema[] => [ + { name: "entity", selector: { entity: { domain: VACUUM_ENTITY_DOMAINS } } }, + { name: "name", selector: { text: {} } }, + { + type: "grid", + name: "", + schema: [ { - type: "grid", - name: "", - schema: [ - { name: "icon", selector: { icon: {} }, context: { icon_entity: "entity" } }, - { name: "icon_animation", selector: { boolean: {} } }, - ], + name: "icon", + selector: { icon: {} }, + context: { icon_entity: "entity" }, }, - ...APPEARANCE_FORM_SCHEMA, - { - name: "commands", - selector: { - select: { - mode: "list", - multiple: true, - options: VACUUM_COMMANDS.map((command) => ({ - value: command, - label: - command === "on_off" - ? customLocalize(`editor.card.vacuum.commands_list.${command}`) - : localize(`ui.dialogs.more_info_control.vacuum.${command}`), - })), - }, - }, + { name: "icon_animation", selector: { boolean: {} } }, + ], + }, + ...APPEARANCE_FORM_SCHEMA, + { + name: "commands", + selector: { + select: { + mode: "list", + multiple: true, + options: VACUUM_COMMANDS.map((command) => ({ + value: command, + label: + command === "on_off" + ? customLocalize(`editor.card.vacuum.commands_list.${command}`) + : localize(`ui.dialogs.more_info_control.vacuum.${command}`), + })), }, - ...computeActionsFormSchema(), - ] + }, + }, + ...computeActionsFormSchema(), + ] ); @customElement(VACUUM_CARD_EDITOR_NAME) -export class VacuumCardEditor extends MushroomBaseElement implements LovelaceCardEditor { - @state() private _config?: VacuumCardConfig; +export class VacuumCardEditor + extends MushroomBaseElement + implements LovelaceCardEditor +{ + @state() private _config?: VacuumCardConfig; - connectedCallback() { - super.connectedCallback(); - void loadHaComponents(); - } + connectedCallback() { + super.connectedCallback(); + void loadHaComponents(); + } - public setConfig(config: VacuumCardConfig): void { - assert(config, vacuumCardConfigStruct); - this._config = config; - } + public setConfig(config: VacuumCardConfig): void { + assert(config, vacuumCardConfigStruct); + this._config = config; + } - private _computeLabel = (schema: HaFormSchema) => { - const customLocalize = setupCustomlocalize(this.hass!); + private _computeLabel = (schema: HaFormSchema) => { + const customLocalize = setupCustomlocalize(this.hass!); - if (GENERIC_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.generic.${schema.name}`); - } - if (VACUUM_LABELS.includes(schema.name)) { - return customLocalize(`editor.card.vacuum.${schema.name}`); - } - return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }; + if (GENERIC_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.generic.${schema.name}`); + } + if (VACUUM_LABELS.includes(schema.name)) { + return customLocalize(`editor.card.vacuum.${schema.name}`); + } + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; - protected render() { - if (!this.hass || !this._config) { - return nothing; - } + protected render() { + if (!this.hass || !this._config) { + return nothing; + } - const customLocalize = setupCustomlocalize(this.hass!); - const schema = computeSchema(this.hass!.localize, customLocalize); + const customLocalize = setupCustomlocalize(this.hass!); + const schema = computeSchema(this.hass!.localize, customLocalize); - return html` - - `; - } + return html` + + `; + } - private _valueChanged(ev: CustomEvent): void { - fireEvent(this, "config-changed", { config: ev.detail.value }); - } + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } } diff --git a/src/cards/vacuum-card/vacuum-card.ts b/src/cards/vacuum-card/vacuum-card.ts index e325cea11..10115e8cf 100644 --- a/src/cards/vacuum-card/vacuum-card.ts +++ b/src/cards/vacuum-card/vacuum-card.ts @@ -4,16 +4,16 @@ import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { - actionHandler, - ActionHandlerEvent, - computeRTL, - handleAction, - hasAction, - HomeAssistant, - isActive, - LovelaceCard, - LovelaceCardEditor, - VacuumEntity, + actionHandler, + ActionHandlerEvent, + computeRTL, + handleAction, + hasAction, + HomeAssistant, + isActive, + LovelaceCard, + LovelaceCardEditor, + VacuumEntity, } from "../../ha"; import "../../shared/badge-icon"; import "../../shared/card"; @@ -25,145 +25,164 @@ import { MushroomBaseCard } from "../../utils/base-card"; import { cardStyle } from "../../utils/card-styles"; import { registerCustomCard } from "../../utils/custom-cards"; import { computeEntityPicture } from "../../utils/info"; -import { VACUUM_CARD_EDITOR_NAME, VACUUM_CARD_NAME, VACUUM_ENTITY_DOMAINS } from "./const"; +import { + VACUUM_CARD_EDITOR_NAME, + VACUUM_CARD_NAME, + VACUUM_ENTITY_DOMAINS, +} from "./const"; import "./controls/vacuum-commands-control"; import { - isCommandsControlSupported, - isCommandsControlVisible, + isCommandsControlSupported, + isCommandsControlVisible, } from "./controls/vacuum-commands-control"; import { isCleaning, isReturningHome } from "./utils"; import { VacuumCardConfig } from "./vacuum-card-config"; registerCustomCard({ - type: VACUUM_CARD_NAME, - name: "Mushroom Vacuum Card", - description: "Card for vacuum entity", + type: VACUUM_CARD_NAME, + name: "Mushroom Vacuum Card", + description: "Card for vacuum entity", }); @customElement(VACUUM_CARD_NAME) export class VacuumCard - extends MushroomBaseCard - implements LovelaceCard + extends MushroomBaseCard + implements LovelaceCard { - public static async getConfigElement(): Promise { - await import("./vacuum-card-editor"); - return document.createElement(VACUUM_CARD_EDITOR_NAME) as LovelaceCardEditor; - } + public static async getConfigElement(): Promise { + await import("./vacuum-card-editor"); + return document.createElement( + VACUUM_CARD_EDITOR_NAME + ) as LovelaceCardEditor; + } - public static async getStubConfig(hass: HomeAssistant): Promise { - const entities = Object.keys(hass.states); - const vacuums = entities.filter((e) => VACUUM_ENTITY_DOMAINS.includes(e.split(".")[0])); - return { - type: `custom:${VACUUM_CARD_NAME}`, - entity: vacuums[0], - }; - } + public static async getStubConfig( + hass: HomeAssistant + ): Promise { + const entities = Object.keys(hass.states); + const vacuums = entities.filter((e) => + VACUUM_ENTITY_DOMAINS.includes(e.split(".")[0]) + ); + return { + type: `custom:${VACUUM_CARD_NAME}`, + entity: vacuums[0], + }; + } - protected get hasControls() { - if (!this._stateObj || !this._config) return false; - return isCommandsControlSupported(this._stateObj, this._config.commands ?? []); - } + protected get hasControls() { + if (!this._stateObj || !this._config) return false; + return isCommandsControlSupported( + this._stateObj, + this._config.commands ?? [] + ); + } - private _handleAction(ev: ActionHandlerEvent) { - handleAction(this, this.hass!, this._config!, ev.detail.action!); - } + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } - protected render() { - if (!this._config || !this.hass || !this._config.entity) { - return nothing; - } + protected render() { + if (!this._config || !this.hass || !this._config.entity) { + return nothing; + } - const stateObj = this._stateObj; + const stateObj = this._stateObj; - if (!stateObj) { - return this.renderNotFound(this._config); - } + if (!stateObj) { + return this.renderNotFound(this._config); + } - const name = this._config.name || stateObj.attributes.friendly_name || ""; - const icon = this._config.icon; - const appearance = computeAppearance(this._config); - const picture = computeEntityPicture(stateObj, appearance.icon_type); + const name = this._config.name || stateObj.attributes.friendly_name || ""; + const icon = this._config.icon; + const appearance = computeAppearance(this._config); + const picture = computeEntityPicture(stateObj, appearance.icon_type); - const rtl = computeRTL(this.hass); + const rtl = computeRTL(this.hass); - const commands = this._config?.commands ?? []; + const commands = this._config?.commands ?? []; - return html` - - - - ${picture ? this.renderPicture(picture) : this.renderIcon(stateObj, icon)} - ${this.renderBadge(stateObj)} - ${this.renderStateInfo(stateObj, appearance, name)}; - - ${isCommandsControlVisible(stateObj, commands) - ? html` -
- - -
- ` - : nothing} -
-
- `; - } - - protected renderIcon(stateObj: HassEntity, icon?: string): TemplateResult { - return html` - - + + + ${picture + ? this.renderPicture(picture) + : this.renderIcon(stateObj, icon)} + ${this.renderBadge(stateObj)} + ${this.renderStateInfo(stateObj, appearance, name)}; + + ${isCommandsControlVisible(stateObj, commands) + ? html` +
+ - `; - } + .entity=${stateObj} + .commands=${commands} + .fill=${appearance.layout !== "horizontal"} + > + +
+ ` + : nothing} +
+ + `; + } - static get styles(): CSSResultGroup { - return [ - super.styles, - cardStyle, - css` - mushroom-state-item { - cursor: pointer; - } - mushroom-shape-icon { - --icon-color: rgb(var(--rgb-state-vacuum)); - --shape-color: rgba(var(--rgb-state-vacuum), 0.2); - } - .cleaning ha-state-icon { - animation: 5s infinite linear cleaning; - } - .cleaning ha-state-icon { - animation: 2s infinite linear returning; - } - mushroom-vacuum-commands-control { - flex: 1; - } - `, - ]; - } + protected renderIcon(stateObj: HassEntity, icon?: string): TemplateResult { + return html` + + + `; + } + + static get styles(): CSSResultGroup { + return [ + super.styles, + cardStyle, + css` + mushroom-state-item { + cursor: pointer; + } + mushroom-shape-icon { + --icon-color: rgb(var(--rgb-state-vacuum)); + --shape-color: rgba(var(--rgb-state-vacuum), 0.2); + } + .cleaning ha-state-icon { + animation: 5s infinite linear cleaning; + } + .cleaning ha-state-icon { + animation: 2s infinite linear returning; + } + mushroom-vacuum-commands-control { + flex: 1; + } + `, + ]; + } } diff --git a/src/ha/common/datetime/duration.ts b/src/ha/common/datetime/duration.ts index 868ea00f2..36fef1902 100644 --- a/src/ha/common/datetime/duration.ts +++ b/src/ha/common/datetime/duration.ts @@ -6,12 +6,14 @@ const MINUTE_IN_MILLISECONDS = 60000; const SECOND_IN_MILLISECONDS = 1000; export const UNIT_TO_MILLISECOND_CONVERT = { - ms: 1, - s: SECOND_IN_MILLISECONDS, - min: MINUTE_IN_MILLISECONDS, - h: HOUR_IN_MILLISECONDS, - d: DAY_IN_MILLISECONDS, + ms: 1, + s: SECOND_IN_MILLISECONDS, + min: MINUTE_IN_MILLISECONDS, + h: HOUR_IN_MILLISECONDS, + d: DAY_IN_MILLISECONDS, }; export const formatDuration = (duration: string, units: string): string => - millisecondsToDuration(parseFloat(duration) * UNIT_TO_MILLISECOND_CONVERT[units]) || "0"; + millisecondsToDuration( + parseFloat(duration) * UNIT_TO_MILLISECOND_CONVERT[units] + ) || "0"; diff --git a/src/ha/common/datetime/format_date.ts b/src/ha/common/datetime/format_date.ts index 5770afe9f..95ca1a94c 100644 --- a/src/ha/common/datetime/format_date.ts +++ b/src/ha/common/datetime/format_date.ts @@ -4,166 +4,190 @@ import { FrontendLocaleData, DateFormat } from "../../data/translation"; // Tuesday, August 10 export const formatDateWeekdayDay = ( - dateObj: Date, - locale: FrontendLocaleData, - config: HassConfig + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig ) => formatDateWeekdayDayMem(locale, config.time_zone).format(dateObj); const formatDateWeekdayDayMem = memoizeOne( - (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat(locale.language, { - weekday: "long", - month: "long", - day: "numeric", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - }) + (locale: FrontendLocaleData, serverTimeZone: string) => + new Intl.DateTimeFormat(locale.language, { + weekday: "long", + month: "long", + day: "numeric", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // August 10, 2021 -export const formatDate = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) => - formatDateMem(locale, config.time_zone).format(dateObj); +export const formatDate = ( + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig +) => formatDateMem(locale, config.time_zone).format(dateObj); const formatDateMem = memoizeOne( - (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat(locale.language, { - year: "numeric", - month: "long", - day: "numeric", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - }) + (locale: FrontendLocaleData, serverTimeZone: string) => + new Intl.DateTimeFormat(locale.language, { + year: "numeric", + month: "long", + day: "numeric", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // 10/08/2021 export const formatDateNumeric = ( - dateObj: Date, - locale: FrontendLocaleData, - config: HassConfig + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig ) => { - const formatter = formatDateNumericMem(locale, config.time_zone); + const formatter = formatDateNumericMem(locale, config.time_zone); - if (locale.date_format === DateFormat.language || locale.date_format === DateFormat.system) { - return formatter.format(dateObj); - } + if ( + locale.date_format === DateFormat.language || + locale.date_format === DateFormat.system + ) { + return formatter.format(dateObj); + } - const parts = formatter.formatToParts(dateObj); + const parts = formatter.formatToParts(dateObj); - const literal = parts.find((value) => value.type === "literal")?.value; - const day = parts.find((value) => value.type === "day")?.value; - const month = parts.find((value) => value.type === "month")?.value; - const year = parts.find((value) => value.type === "year")?.value; + const literal = parts.find((value) => value.type === "literal")?.value; + const day = parts.find((value) => value.type === "day")?.value; + const month = parts.find((value) => value.type === "month")?.value; + const year = parts.find((value) => value.type === "year")?.value; - const lastPart = parts[parts.length - 1]; - let lastLiteral = lastPart?.type === "literal" ? lastPart?.value : ""; + const lastPart = parts[parts.length - 1]; + let lastLiteral = lastPart?.type === "literal" ? lastPart?.value : ""; - if (locale.language === "bg" && locale.date_format === DateFormat.YMD) { - lastLiteral = ""; - } + if (locale.language === "bg" && locale.date_format === DateFormat.YMD) { + lastLiteral = ""; + } - const formats = { - [DateFormat.DMY]: `${day}${literal}${month}${literal}${year}${lastLiteral}`, - [DateFormat.MDY]: `${month}${literal}${day}${literal}${year}${lastLiteral}`, - [DateFormat.YMD]: `${year}${literal}${month}${literal}${day}${lastLiteral}`, - }; + const formats = { + [DateFormat.DMY]: `${day}${literal}${month}${literal}${year}${lastLiteral}`, + [DateFormat.MDY]: `${month}${literal}${day}${literal}${year}${lastLiteral}`, + [DateFormat.YMD]: `${year}${literal}${month}${literal}${day}${lastLiteral}`, + }; - return formats[locale.date_format]; + return formats[locale.date_format]; }; -const formatDateNumericMem = memoizeOne((locale: FrontendLocaleData, serverTimeZone: string) => { - const localeString = locale.date_format === DateFormat.system ? undefined : locale.language; +const formatDateNumericMem = memoizeOne( + (locale: FrontendLocaleData, serverTimeZone: string) => { + const localeString = + locale.date_format === DateFormat.system ? undefined : locale.language; - if (locale.date_format === DateFormat.language || locale.date_format === DateFormat.system) { - return new Intl.DateTimeFormat(localeString, { - year: "numeric", - month: "numeric", - day: "numeric", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - }); - } - - return new Intl.DateTimeFormat(localeString, { + if ( + locale.date_format === DateFormat.language || + locale.date_format === DateFormat.system + ) { + return new Intl.DateTimeFormat(localeString, { year: "numeric", month: "numeric", day: "numeric", timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }); + } + + return new Intl.DateTimeFormat(localeString, { + year: "numeric", + month: "numeric", + day: "numeric", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, }); -}); + } +); // Aug 10 -export const formatDateShort = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) => - formatDateShortMem(locale, config.time_zone).format(dateObj); +export const formatDateShort = ( + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig +) => formatDateShortMem(locale, config.time_zone).format(dateObj); const formatDateShortMem = memoizeOne( - (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat(locale.language, { - day: "numeric", - month: "short", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - }) + (locale: FrontendLocaleData, serverTimeZone: string) => + new Intl.DateTimeFormat(locale.language, { + day: "numeric", + month: "short", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // August 2021 export const formatDateMonthYear = ( - dateObj: Date, - locale: FrontendLocaleData, - config: HassConfig + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig ) => formatDateMonthYearMem(locale, config.time_zone).format(dateObj); const formatDateMonthYearMem = memoizeOne( - (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat(locale.language, { - month: "long", - year: "numeric", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - }) + (locale: FrontendLocaleData, serverTimeZone: string) => + new Intl.DateTimeFormat(locale.language, { + month: "long", + year: "numeric", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // August -export const formatDateMonth = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) => - formatDateMonthMem(locale, config.time_zone).format(dateObj); +export const formatDateMonth = ( + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig +) => formatDateMonthMem(locale, config.time_zone).format(dateObj); const formatDateMonthMem = memoizeOne( - (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat(locale.language, { - month: "long", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - }) + (locale: FrontendLocaleData, serverTimeZone: string) => + new Intl.DateTimeFormat(locale.language, { + month: "long", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // 2021 -export const formatDateYear = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) => - formatDateYearMem(locale, config.time_zone).format(dateObj); +export const formatDateYear = ( + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig +) => formatDateYearMem(locale, config.time_zone).format(dateObj); const formatDateYearMem = memoizeOne( - (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat(locale.language, { - year: "numeric", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - }) + (locale: FrontendLocaleData, serverTimeZone: string) => + new Intl.DateTimeFormat(locale.language, { + year: "numeric", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // Monday -export const formatDateWeekday = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) => - formatDateWeekdayMem(locale, config.time_zone).format(dateObj); +export const formatDateWeekday = ( + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig +) => formatDateWeekdayMem(locale, config.time_zone).format(dateObj); const formatDateWeekdayMem = memoizeOne( - (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat(locale.language, { - weekday: "long", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - }) + (locale: FrontendLocaleData, serverTimeZone: string) => + new Intl.DateTimeFormat(locale.language, { + weekday: "long", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); // Mon export const formatDateWeekdayShort = ( - dateObj: Date, - locale: FrontendLocaleData, - config: HassConfig + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig ) => formatDateWeekdayShortMem(locale, config.time_zone).format(dateObj); const formatDateWeekdayShortMem = memoizeOne( - (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat(locale.language, { - weekday: "short", - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - }) + (locale: FrontendLocaleData, serverTimeZone: string) => + new Intl.DateTimeFormat(locale.language, { + weekday: "short", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); diff --git a/src/ha/common/datetime/format_date_time.ts b/src/ha/common/datetime/format_date_time.ts index 6b771c762..f70c685a1 100644 --- a/src/ha/common/datetime/format_date_time.ts +++ b/src/ha/common/datetime/format_date_time.ts @@ -6,97 +6,109 @@ import { formatTime } from "./format_time"; import { useAmPm } from "./use_am_pm"; // August 9, 2021, 8:23 AM -export const formatDateTime = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) => - formatDateTimeMem(locale, config.time_zone).format(dateObj); +export const formatDateTime = ( + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig +) => formatDateTimeMem(locale, config.time_zone).format(dateObj); const formatDateTimeMem = memoizeOne( - (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) ? "en-u-hc-h23" : locale.language, - { - year: "numeric", - month: "long", - day: "numeric", - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + (locale: FrontendLocaleData, serverTimeZone: string) => + new Intl.DateTimeFormat( + locale.language === "en" && !useAmPm(locale) + ? "en-u-hc-h23" + : locale.language, + { + year: "numeric", + month: "long", + day: "numeric", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + hour12: useAmPm(locale), + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + } + ) ); // Aug 9, 2021, 8:23 AM export const formatShortDateTimeWithYear = ( - dateObj: Date, - locale: FrontendLocaleData, - config: HassConfig + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig ) => formatShortDateTimeWithYearMem(locale, config.time_zone).format(dateObj); const formatShortDateTimeWithYearMem = memoizeOne( - (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) ? "en-u-hc-h23" : locale.language, - { - year: "numeric", - month: "short", - day: "numeric", - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + (locale: FrontendLocaleData, serverTimeZone: string) => + new Intl.DateTimeFormat( + locale.language === "en" && !useAmPm(locale) + ? "en-u-hc-h23" + : locale.language, + { + year: "numeric", + month: "short", + day: "numeric", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + hour12: useAmPm(locale), + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + } + ) ); // Aug 9, 8:23 AM export const formatShortDateTime = ( - dateObj: Date, - locale: FrontendLocaleData, - config: HassConfig + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig ) => formatShortDateTimeMem(locale, config.time_zone).format(dateObj); const formatShortDateTimeMem = memoizeOne( - (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) ? "en-u-hc-h23" : locale.language, - { - month: "short", - day: "numeric", - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + (locale: FrontendLocaleData, serverTimeZone: string) => + new Intl.DateTimeFormat( + locale.language === "en" && !useAmPm(locale) + ? "en-u-hc-h23" + : locale.language, + { + month: "short", + day: "numeric", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + hour12: useAmPm(locale), + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + } + ) ); // August 9, 2021, 8:23:15 AM export const formatDateTimeWithSeconds = ( - dateObj: Date, - locale: FrontendLocaleData, - config: HassConfig + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig ) => formatDateTimeWithSecondsMem(locale, config.time_zone).format(dateObj); const formatDateTimeWithSecondsMem = memoizeOne( - (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) ? "en-u-hc-h23" : locale.language, - { - year: "numeric", - month: "long", - day: "numeric", - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + (locale: FrontendLocaleData, serverTimeZone: string) => + new Intl.DateTimeFormat( + locale.language === "en" && !useAmPm(locale) + ? "en-u-hc-h23" + : locale.language, + { + year: "numeric", + month: "long", + day: "numeric", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: useAmPm(locale), + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + } + ) ); // 9/8/2021, 8:23 AM export const formatDateTimeNumeric = ( - dateObj: Date, - locale: FrontendLocaleData, - config: HassConfig -) => `${formatDateNumeric(dateObj, locale, config)}, ${formatTime(dateObj, locale, config)}`; + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig +) => + `${formatDateNumeric(dateObj, locale, config)}, ${formatTime(dateObj, locale, config)}`; diff --git a/src/ha/common/datetime/format_time.ts b/src/ha/common/datetime/format_time.ts index d7d5ce8d9..3bda190f5 100644 --- a/src/ha/common/datetime/format_time.ts +++ b/src/ha/common/datetime/format_time.ts @@ -4,72 +4,87 @@ import { FrontendLocaleData } from "../../data/translation"; import { useAmPm } from "./use_am_pm"; // 9:15 PM || 21:15 -export const formatTime = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) => - formatTimeMem(locale, config.time_zone).format(dateObj); +export const formatTime = ( + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig +) => formatTimeMem(locale, config.time_zone).format(dateObj); const formatTimeMem = memoizeOne( - (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) ? "en-u-hc-h23" : locale.language, - { - hour: "numeric", - minute: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + (locale: FrontendLocaleData, serverTimeZone: string) => + new Intl.DateTimeFormat( + locale.language === "en" && !useAmPm(locale) + ? "en-u-hc-h23" + : locale.language, + { + hour: "numeric", + minute: "2-digit", + hour12: useAmPm(locale), + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + } + ) ); // 9:15:24 PM || 21:15:24 export const formatTimeWithSeconds = ( - dateObj: Date, - locale: FrontendLocaleData, - config: HassConfig + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig ) => formatTimeWithSecondsMem(locale, config.time_zone).format(dateObj); const formatTimeWithSecondsMem = memoizeOne( - (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) ? "en-u-hc-h23" : locale.language, - { - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + (locale: FrontendLocaleData, serverTimeZone: string) => + new Intl.DateTimeFormat( + locale.language === "en" && !useAmPm(locale) + ? "en-u-hc-h23" + : locale.language, + { + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: useAmPm(locale), + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + } + ) ); // Tuesday 7:00 PM || Tuesday 19:00 -export const formatTimeWeekday = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) => - formatTimeWeekdayMem(locale, config.time_zone).format(dateObj); +export const formatTimeWeekday = ( + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig +) => formatTimeWeekdayMem(locale, config.time_zone).format(dateObj); const formatTimeWeekdayMem = memoizeOne( - (locale: FrontendLocaleData, serverTimeZone: string) => - new Intl.DateTimeFormat( - locale.language === "en" && !useAmPm(locale) ? "en-u-hc-h23" : locale.language, - { - weekday: "long", - hour: useAmPm(locale) ? "numeric" : "2-digit", - minute: "2-digit", - hour12: useAmPm(locale), - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - } - ) + (locale: FrontendLocaleData, serverTimeZone: string) => + new Intl.DateTimeFormat( + locale.language === "en" && !useAmPm(locale) + ? "en-u-hc-h23" + : locale.language, + { + weekday: "long", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + hour12: useAmPm(locale), + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + } + ) ); // 21:15 -export const formatTime24h = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) => - formatTime24hMem(locale, config.time_zone).format(dateObj); +export const formatTime24h = ( + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig +) => formatTime24hMem(locale, config.time_zone).format(dateObj); const formatTime24hMem = memoizeOne( - (locale: FrontendLocaleData, serverTimeZone: string) => - // en-GB to fix Chrome 24:59 to 0:59 https://stackoverflow.com/a/60898146 - new Intl.DateTimeFormat("en-GB", { - hour: "numeric", - minute: "2-digit", - hour12: false, - timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, - }) + (locale: FrontendLocaleData, serverTimeZone: string) => + // en-GB to fix Chrome 24:59 to 0:59 https://stackoverflow.com/a/60898146 + new Intl.DateTimeFormat("en-GB", { + hour: "numeric", + minute: "2-digit", + hour12: false, + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) ); diff --git a/src/ha/common/datetime/milliseconds_to_duration.ts b/src/ha/common/datetime/milliseconds_to_duration.ts index 552a577c7..384a8a770 100644 --- a/src/ha/common/datetime/milliseconds_to_duration.ts +++ b/src/ha/common/datetime/milliseconds_to_duration.ts @@ -1,25 +1,25 @@ const leftPad = (num: number, digits = 2) => { - let paddedNum = "" + num; - for (let i = 1; i < digits; i++) { - paddedNum = parseInt(paddedNum) < 10 ** i ? `0${paddedNum}` : paddedNum; - } - return paddedNum; + let paddedNum = "" + num; + for (let i = 1; i < digits; i++) { + paddedNum = parseInt(paddedNum) < 10 ** i ? `0${paddedNum}` : paddedNum; + } + return paddedNum; }; export default function millisecondsToDuration(d: number) { - const h = Math.floor(d / 1000 / 3600); - const m = Math.floor(((d / 1000) % 3600) / 60); - const s = Math.floor(((d / 1000) % 3600) % 60); - const ms = Math.floor(d % 1000); + const h = Math.floor(d / 1000 / 3600); + const m = Math.floor(((d / 1000) % 3600) / 60); + const s = Math.floor(((d / 1000) % 3600) % 60); + const ms = Math.floor(d % 1000); - if (h > 0) { - return `${h}:${leftPad(m)}:${leftPad(s)}`; - } - if (m > 0) { - return `${m}:${leftPad(s)}`; - } - if (s > 0 || ms > 0) { - return `${s}${ms > 0 ? `.${leftPad(ms, 3)}` : ``}`; - } - return null; + if (h > 0) { + return `${h}:${leftPad(m)}:${leftPad(s)}`; + } + if (m > 0) { + return `${m}:${leftPad(s)}`; + } + if (s > 0 || ms > 0) { + return `${s}${ms > 0 ? `.${leftPad(ms, 3)}` : ``}`; + } + return null; } diff --git a/src/ha/common/datetime/use_am_pm.ts b/src/ha/common/datetime/use_am_pm.ts index dd53415b7..97bf2911b 100644 --- a/src/ha/common/datetime/use_am_pm.ts +++ b/src/ha/common/datetime/use_am_pm.ts @@ -2,12 +2,15 @@ import memoizeOne from "memoize-one"; import { FrontendLocaleData, TimeFormat } from "../../data/translation"; export const useAmPm = memoizeOne((locale: FrontendLocaleData): boolean => { - if (locale.time_format === TimeFormat.language || locale.time_format === TimeFormat.system) { - const testLanguage = - locale.time_format === TimeFormat.language ? locale.language : undefined; - const test = new Date().toLocaleString(testLanguage); - return test.includes("AM") || test.includes("PM"); - } + if ( + locale.time_format === TimeFormat.language || + locale.time_format === TimeFormat.system + ) { + const testLanguage = + locale.time_format === TimeFormat.language ? locale.language : undefined; + const test = new Date().toLocaleString(testLanguage); + return test.includes("AM") || test.includes("PM"); + } - return locale.time_format === TimeFormat.am_pm; + return locale.time_format === TimeFormat.am_pm; }); diff --git a/src/ha/common/dom/fire_event.ts b/src/ha/common/dom/fire_event.ts index 686ca825a..a72bb67ea 100644 --- a/src/ha/common/dom/fire_event.ts +++ b/src/ha/common/dom/fire_event.ts @@ -29,14 +29,14 @@ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. declare global { - // eslint-disable-next-line - interface HASSDomEvents {} + // eslint-disable-next-line + interface HASSDomEvents {} } export type ValidHassDomEvent = keyof HASSDomEvents; export interface HASSDomEvent extends Event { - detail: T; + detail: T; } /** @@ -55,24 +55,24 @@ export interface HASSDomEvent extends Event { * @return {Event} The new event that was fired. */ export const fireEvent = ( - node: HTMLElement | Window, - type: HassEvent, - detail?: HASSDomEvents[HassEvent], - options?: { - bubbles?: boolean; - cancelable?: boolean; - composed?: boolean; - } + node: HTMLElement | Window, + type: HassEvent, + detail?: HASSDomEvents[HassEvent], + options?: { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; + } ) => { - options = options || {}; - // @ts-ignore - detail = detail === null || detail === undefined ? {} : detail; - const event = new Event(type, { - bubbles: options.bubbles === undefined ? true : options.bubbles, - cancelable: Boolean(options.cancelable), - composed: options.composed === undefined ? true : options.composed, - }); - (event as any).detail = detail; - node.dispatchEvent(event); - return event; + options = options || {}; + // @ts-ignore + detail = detail === null || detail === undefined ? {} : detail; + const event = new Event(type, { + bubbles: options.bubbles === undefined ? true : options.bubbles, + cancelable: Boolean(options.cancelable), + composed: options.composed === undefined ? true : options.composed, + }); + (event as any).detail = detail; + node.dispatchEvent(event); + return event; }; diff --git a/src/ha/common/dom/get_main_window.ts b/src/ha/common/dom/get_main_window.ts index e7262c6e4..a2a5cca7b 100644 --- a/src/ha/common/dom/get_main_window.ts +++ b/src/ha/common/dom/get_main_window.ts @@ -1,4 +1,8 @@ import { MAIN_WINDOW_NAME } from "../../data/main_window"; export const mainWindow = - window.name === MAIN_WINDOW_NAME ? window : parent.name === MAIN_WINDOW_NAME ? parent : top!; + window.name === MAIN_WINDOW_NAME + ? window + : parent.name === MAIN_WINDOW_NAME + ? parent + : top!; diff --git a/src/ha/common/entity/compute_domain.ts b/src/ha/common/entity/compute_domain.ts index 447567ff7..29e568885 100644 --- a/src/ha/common/entity/compute_domain.ts +++ b/src/ha/common/entity/compute_domain.ts @@ -1,2 +1,2 @@ export const computeDomain = (entityId: string): string => - entityId.substr(0, entityId.indexOf(".")); + entityId.substr(0, entityId.indexOf(".")); diff --git a/src/ha/common/entity/compute_object_id.ts b/src/ha/common/entity/compute_object_id.ts index aa1cf2efc..eb3030643 100644 --- a/src/ha/common/entity/compute_object_id.ts +++ b/src/ha/common/entity/compute_object_id.ts @@ -1,3 +1,3 @@ /** Compute the object ID of a state. */ export const computeObjectId = (entityId: string): string => - entityId.substr(entityId.indexOf(".") + 1); + entityId.substr(entityId.indexOf(".") + 1); diff --git a/src/ha/common/entity/compute_state_display.ts b/src/ha/common/entity/compute_state_display.ts index 4b7cbd48f..26bc0e830 100644 --- a/src/ha/common/entity/compute_state_display.ts +++ b/src/ha/common/entity/compute_state_display.ts @@ -1,16 +1,22 @@ import { HassConfig, HassEntity } from "home-assistant-js-websocket"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; -import { updateIsInstallingFromAttributes, UPDATE_SUPPORT_PROGRESS } from "../../data/update"; +import { + updateIsInstallingFromAttributes, + UPDATE_SUPPORT_PROGRESS, +} from "../../data/update"; import { EntityRegistryDisplayEntry, HomeAssistant } from "../../types"; import { FrontendLocaleData, TimeZone } from "../../data/translation"; -import { UNIT_TO_MILLISECOND_CONVERT, formatDuration } from "../datetime/duration"; +import { + UNIT_TO_MILLISECOND_CONVERT, + formatDuration, +} from "../datetime/duration"; import { formatDate } from "../datetime/format_date"; import { formatDateTime } from "../datetime/format_date_time"; import { formatTime } from "../datetime/format_time"; import { - formatNumber, - getNumberFormatOptions, - isNumericFromAttributes, + formatNumber, + getNumberFormatOptions, + isNumericFromAttributes, } from "../number/format_number"; import { blankBeforePercent } from "../translations/blank_before_percent"; import { LocalizeFunc } from "../translations/localize"; @@ -18,201 +24,212 @@ import { computeDomain } from "./compute_domain"; import { supportsFeatureFromAttributes } from "./supports-feature"; export const computeStateDisplaySingleEntity = ( - localize: LocalizeFunc, - stateObj: HassEntity, - locale: FrontendLocaleData, - config: HassConfig, - entity: EntityRegistryDisplayEntry | undefined, - state?: string + localize: LocalizeFunc, + stateObj: HassEntity, + locale: FrontendLocaleData, + config: HassConfig, + entity: EntityRegistryDisplayEntry | undefined, + state?: string ): string => - computeStateDisplayFromEntityAttributes( - localize, - locale, - config, - entity, - stateObj.entity_id, - stateObj.attributes, - state !== undefined ? state : stateObj.state - ); + computeStateDisplayFromEntityAttributes( + localize, + locale, + config, + entity, + stateObj.entity_id, + stateObj.attributes, + state !== undefined ? state : stateObj.state + ); export const computeStateDisplay = ( - localize: LocalizeFunc, - stateObj: HassEntity, - locale: FrontendLocaleData, - config: HassConfig, - entities: HomeAssistant["entities"], - state?: string + localize: LocalizeFunc, + stateObj: HassEntity, + locale: FrontendLocaleData, + config: HassConfig, + entities: HomeAssistant["entities"], + state?: string ): string => { - const entity = entities[stateObj.entity_id] as EntityRegistryDisplayEntry | undefined; + const entity = entities[stateObj.entity_id] as + | EntityRegistryDisplayEntry + | undefined; - return computeStateDisplayFromEntityAttributes( - localize, - locale, - config, - entity, - stateObj.entity_id, - stateObj.attributes, - state !== undefined ? state : stateObj.state - ); + return computeStateDisplayFromEntityAttributes( + localize, + locale, + config, + entity, + stateObj.entity_id, + stateObj.attributes, + state !== undefined ? state : stateObj.state + ); }; export const computeStateDisplayFromEntityAttributes = ( - localize: LocalizeFunc, - locale: FrontendLocaleData, - config: HassConfig, - entity: EntityRegistryDisplayEntry | undefined, - entityId: string, - attributes: any, - state: string + localize: LocalizeFunc, + locale: FrontendLocaleData, + config: HassConfig, + entity: EntityRegistryDisplayEntry | undefined, + entityId: string, + attributes: any, + state: string ): string => { - if (state === UNKNOWN || state === UNAVAILABLE) { - return localize(`state.default.${state}`); - } + if (state === UNKNOWN || state === UNAVAILABLE) { + return localize(`state.default.${state}`); + } - // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` - if (isNumericFromAttributes(attributes)) { - // state is duration - if ( - attributes.device_class === "duration" && - attributes.unit_of_measurement && - UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement] - ) { - try { - return formatDuration(state, attributes.unit_of_measurement); - } catch (_err) { - // fallback to default - } - } - if (attributes.device_class === "monetary") { - try { - return formatNumber(state, locale, { - style: "currency", - currency: attributes.unit_of_measurement, - minimumFractionDigits: 2, - // Override monetary options with number format - ...getNumberFormatOptions({ state, attributes } as HassEntity, entity), - }); - } catch (_err) { - // fallback to default - } - } - const unit = !attributes.unit_of_measurement - ? "" - : attributes.unit_of_measurement === "%" - ? blankBeforePercent(locale) + "%" - : ` ${attributes.unit_of_measurement}`; - return `${formatNumber( - state, - locale, - getNumberFormatOptions({ state, attributes } as HassEntity, entity) - )}${unit}`; + // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` + if (isNumericFromAttributes(attributes)) { + // state is duration + if ( + attributes.device_class === "duration" && + attributes.unit_of_measurement && + UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement] + ) { + try { + return formatDuration(state, attributes.unit_of_measurement); + } catch (_err) { + // fallback to default + } } + if (attributes.device_class === "monetary") { + try { + return formatNumber(state, locale, { + style: "currency", + currency: attributes.unit_of_measurement, + minimumFractionDigits: 2, + // Override monetary options with number format + ...getNumberFormatOptions( + { state, attributes } as HassEntity, + entity + ), + }); + } catch (_err) { + // fallback to default + } + } + const unit = !attributes.unit_of_measurement + ? "" + : attributes.unit_of_measurement === "%" + ? blankBeforePercent(locale) + "%" + : ` ${attributes.unit_of_measurement}`; + return `${formatNumber( + state, + locale, + getNumberFormatOptions({ state, attributes } as HassEntity, entity) + )}${unit}`; + } - const domain = computeDomain(entityId); + const domain = computeDomain(entityId); - if (domain === "datetime") { - const time = new Date(state); - return formatDateTime(time, locale, config); - } + if (domain === "datetime") { + const time = new Date(state); + return formatDateTime(time, locale, config); + } - if (["date", "input_datetime", "time"].includes(domain)) { - // If trying to display an explicit state, need to parse the explicit state to `Date` then format. - // Attributes aren't available, we have to use `state`. + if (["date", "input_datetime", "time"].includes(domain)) { + // If trying to display an explicit state, need to parse the explicit state to `Date` then format. + // Attributes aren't available, we have to use `state`. - // These are timezone agnostic, so we should NOT use the system timezone here. - try { - const components = state.split(" "); - if (components.length === 2) { - // Date and time. - return formatDateTime( - new Date(components.join("T")), - { ...locale, time_zone: TimeZone.local }, - config - ); - } - if (components.length === 1) { - if (state.includes("-")) { - // Date only. - return formatDate( - new Date(`${state}T00:00`), - { ...locale, time_zone: TimeZone.local }, - config - ); - } - if (state.includes(":")) { - // Time only. - const now = new Date(); - return formatTime( - new Date(`${now.toISOString().split("T")[0]}T${state}`), - { ...locale, time_zone: TimeZone.local }, - config - ); - } - } - return state; - } catch (_e) { - // Formatting methods may throw error if date parsing doesn't go well, - // just return the state string in that case. - return state; + // These are timezone agnostic, so we should NOT use the system timezone here. + try { + const components = state.split(" "); + if (components.length === 2) { + // Date and time. + return formatDateTime( + new Date(components.join("T")), + { ...locale, time_zone: TimeZone.local }, + config + ); + } + if (components.length === 1) { + if (state.includes("-")) { + // Date only. + return formatDate( + new Date(`${state}T00:00`), + { ...locale, time_zone: TimeZone.local }, + config + ); + } + if (state.includes(":")) { + // Time only. + const now = new Date(); + return formatTime( + new Date(`${now.toISOString().split("T")[0]}T${state}`), + { ...locale, time_zone: TimeZone.local }, + config + ); } + } + return state; + } catch (_e) { + // Formatting methods may throw error if date parsing doesn't go well, + // just return the state string in that case. + return state; } + } - // `counter` `number` and `input_number` domains do not have a unit of measurement but should still use `formatNumber` - if (domain === "counter" || domain === "number" || domain === "input_number") { - // Format as an integer if the value and step are integers - return formatNumber( - state, - locale, - getNumberFormatOptions({ state, attributes } as HassEntity, entity) - ); - } + // `counter` `number` and `input_number` domains do not have a unit of measurement but should still use `formatNumber` + if ( + domain === "counter" || + domain === "number" || + domain === "input_number" + ) { + // Format as an integer if the value and step are integers + return formatNumber( + state, + locale, + getNumberFormatOptions({ state, attributes } as HassEntity, entity) + ); + } - // state is a timestamp - if ( - ["button", "event", "input_button", "scene", "stt", "tts"].includes(domain) || - (domain === "sensor" && attributes.device_class === "timestamp") - ) { - try { - return formatDateTime(new Date(state), locale, config); - } catch (_err) { - return state; - } + // state is a timestamp + if ( + ["button", "event", "input_button", "scene", "stt", "tts"].includes( + domain + ) || + (domain === "sensor" && attributes.device_class === "timestamp") + ) { + try { + return formatDateTime(new Date(state), locale, config); + } catch (_err) { + return state; } + } - if (domain === "update") { - // When updating, and entity does not support % show "Installing" - // When updating, and entity does support % show "Installing (xx%)" - // When update available, show the version - // When the latest version is skipped, show the latest version - // When update is not available, show "Up-to-date" - // When update is not available and there is no latest_version show "Unavailable" - return state === "on" - ? updateIsInstallingFromAttributes(attributes) - ? supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) && - typeof attributes.in_progress === "number" - ? localize("ui.card.update.installing_with_progress", { - progress: attributes.in_progress, - }) - : localize("ui.card.update.installing") - : attributes.latest_version - : attributes.skipped_version === attributes.latest_version - ? attributes.latest_version ?? localize("state.default.unavailable") - : localize("ui.card.update.up_to_date"); - } + if (domain === "update") { + // When updating, and entity does not support % show "Installing" + // When updating, and entity does support % show "Installing (xx%)" + // When update available, show the version + // When the latest version is skipped, show the latest version + // When update is not available, show "Up-to-date" + // When update is not available and there is no latest_version show "Unavailable" + return state === "on" + ? updateIsInstallingFromAttributes(attributes) + ? supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) && + typeof attributes.in_progress === "number" + ? localize("ui.card.update.installing_with_progress", { + progress: attributes.in_progress, + }) + : localize("ui.card.update.installing") + : attributes.latest_version + : attributes.skipped_version === attributes.latest_version + ? attributes.latest_version ?? localize("state.default.unavailable") + : localize("ui.card.update.up_to_date"); + } - return ( - (entity?.translation_key && - localize( - `component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}` - )) || - // Return device class translation - (attributes.device_class && - localize( - `component.${domain}.entity_component.${attributes.device_class}.state.${state}` - )) || - // Return default translation - localize(`component.${domain}.entity_component._.state.${state}`) || - // We don't know! Return the raw state. - state - ); + return ( + (entity?.translation_key && + localize( + `component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}` + )) || + // Return device class translation + (attributes.device_class && + localize( + `component.${domain}.entity_component.${attributes.device_class}.state.${state}` + )) || + // Return default translation + localize(`component.${domain}.entity_component._.state.${state}`) || + // We don't know! Return the raw state. + state + ); }; diff --git a/src/ha/common/entity/compute_state_domain.ts b/src/ha/common/entity/compute_state_domain.ts index 49f7f0b61..1b972ea22 100644 --- a/src/ha/common/entity/compute_state_domain.ts +++ b/src/ha/common/entity/compute_state_domain.ts @@ -1,4 +1,5 @@ import type { HassEntity } from "home-assistant-js-websocket"; import { computeDomain } from "./compute_domain"; -export const computeStateDomain = (stateObj: HassEntity) => computeDomain(stateObj.entity_id); +export const computeStateDomain = (stateObj: HassEntity) => + computeDomain(stateObj.entity_id); diff --git a/src/ha/common/entity/compute_state_name.ts b/src/ha/common/entity/compute_state_name.ts index 5cab56f5f..2311830a9 100644 --- a/src/ha/common/entity/compute_state_name.ts +++ b/src/ha/common/entity/compute_state_name.ts @@ -2,12 +2,12 @@ import { HassEntity } from "home-assistant-js-websocket"; import { computeObjectId } from "./compute_object_id"; export const computeStateNameFromEntityAttributes = ( - entityId: string, - attributes: { [key: string]: any } + entityId: string, + attributes: { [key: string]: any } ): string => - attributes.friendly_name === undefined - ? computeObjectId(entityId).replace(/_/g, " ") - : attributes.friendly_name || ""; + attributes.friendly_name === undefined + ? computeObjectId(entityId).replace(/_/g, " ") + : attributes.friendly_name || ""; export const computeStateName = (stateObj: HassEntity): string => - computeStateNameFromEntityAttributes(stateObj.entity_id, stateObj.attributes); + computeStateNameFromEntityAttributes(stateObj.entity_id, stateObj.attributes); diff --git a/src/ha/common/entity/supports-feature.ts b/src/ha/common/entity/supports-feature.ts index 0d22342b8..18bf3cd89 100644 --- a/src/ha/common/entity/supports-feature.ts +++ b/src/ha/common/entity/supports-feature.ts @@ -1,13 +1,15 @@ import { HassEntity } from "home-assistant-js-websocket"; -export const supportsFeature = (stateObj: HassEntity, feature: number): boolean => - supportsFeatureFromAttributes(stateObj.attributes, feature); +export const supportsFeature = ( + stateObj: HassEntity, + feature: number +): boolean => supportsFeatureFromAttributes(stateObj.attributes, feature); export const supportsFeatureFromAttributes = ( - attributes: { - [key: string]: any; - }, - feature: number + attributes: { + [key: string]: any; + }, + feature: number ): boolean => - // eslint-disable-next-line no-bitwise - (attributes.supported_features! & feature) !== 0; + // eslint-disable-next-line no-bitwise + (attributes.supported_features! & feature) !== 0; diff --git a/src/ha/common/number/clamp.ts b/src/ha/common/number/clamp.ts index 667720ca7..5591885f2 100644 --- a/src/ha/common/number/clamp.ts +++ b/src/ha/common/number/clamp.ts @@ -1,10 +1,10 @@ export const clamp = (value: number, min: number, max: number) => - Math.min(Math.max(value, min), max); + Math.min(Math.max(value, min), max); // Variant that only applies the clamping to a border if the border is defined export const conditionalClamp = (value: number, min?: number, max?: number) => { - let result: number; - result = min ? Math.max(value, min) : value; - result = max ? Math.min(result, max) : result; - return result; + let result: number; + result = min ? Math.max(value, min) : value; + result = max ? Math.min(result, max) : result; + return result; }; diff --git a/src/ha/common/number/format_number.ts b/src/ha/common/number/format_number.ts index 94c5e266e..cd8e26328 100644 --- a/src/ha/common/number/format_number.ts +++ b/src/ha/common/number/format_number.ts @@ -1,4 +1,7 @@ -import { HassEntity, HassEntityAttributeBase } from "home-assistant-js-websocket"; +import { + HassEntity, + HassEntityAttributeBase, +} from "home-assistant-js-websocket"; import { FrontendLocaleData, NumberFormat } from "../../data/translation"; import { EntityRegistryDisplayEntry } from "../../types"; import { round } from "./round"; @@ -8,26 +11,27 @@ import { round } from "./round"; * @param stateObj The entity state object */ export const isNumericState = (stateObj: HassEntity): boolean => - isNumericFromAttributes(stateObj.attributes); + isNumericFromAttributes(stateObj.attributes); -export const isNumericFromAttributes = (attributes: HassEntityAttributeBase): boolean => - !!attributes.unit_of_measurement || !!attributes.state_class; +export const isNumericFromAttributes = ( + attributes: HassEntityAttributeBase +): boolean => !!attributes.unit_of_measurement || !!attributes.state_class; export const numberFormatToLocale = ( - localeOptions: FrontendLocaleData + localeOptions: FrontendLocaleData ): string | string[] | undefined => { - switch (localeOptions.number_format) { - case NumberFormat.comma_decimal: - return ["en-US", "en"]; // Use United States with fallback to English formatting 1,234,567.89 - case NumberFormat.decimal_comma: - return ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89 - case NumberFormat.space_comma: - return ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89 - case NumberFormat.system: - return undefined; - default: - return localeOptions.language; - } + switch (localeOptions.number_format) { + case NumberFormat.comma_decimal: + return ["en-US", "en"]; // Use United States with fallback to English formatting 1,234,567.89 + case NumberFormat.decimal_comma: + return ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89 + case NumberFormat.space_comma: + return ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89 + case NumberFormat.system: + return undefined; + default: + return localeOptions.language; + } }; /** @@ -38,39 +42,47 @@ export const numberFormatToLocale = ( * @param options Intl.NumberFormatOptions to use */ export const formatNumber = ( - num: string | number, - localeOptions?: FrontendLocaleData, - options?: Intl.NumberFormatOptions + num: string | number, + localeOptions?: FrontendLocaleData, + options?: Intl.NumberFormatOptions ): string => { - const locale = localeOptions ? numberFormatToLocale(localeOptions) : undefined; + const locale = localeOptions + ? numberFormatToLocale(localeOptions) + : undefined; - // Polyfill for Number.isNaN, which is more reliable than the global isNaN() - Number.isNaN = - Number.isNaN || - function isNaN(input) { - return typeof input === "number" && isNaN(input); - }; + // Polyfill for Number.isNaN, which is more reliable than the global isNaN() + Number.isNaN = + Number.isNaN || + function isNaN(input) { + return typeof input === "number" && isNaN(input); + }; - if (localeOptions?.number_format !== NumberFormat.none && !Number.isNaN(Number(num)) && Intl) { - try { - return new Intl.NumberFormat(locale, getDefaultFormatOptions(num, options)).format( - Number(num) - ); - } catch (err: any) { - // Don't fail when using "TEST" language - // eslint-disable-next-line no-console - console.error(err); - return new Intl.NumberFormat(undefined, getDefaultFormatOptions(num, options)).format( - Number(num) - ); - } + if ( + localeOptions?.number_format !== NumberFormat.none && + !Number.isNaN(Number(num)) && + Intl + ) { + try { + return new Intl.NumberFormat( + locale, + getDefaultFormatOptions(num, options) + ).format(Number(num)); + } catch (err: any) { + // Don't fail when using "TEST" language + // eslint-disable-next-line no-console + console.error(err); + return new Intl.NumberFormat( + undefined, + getDefaultFormatOptions(num, options) + ).format(Number(num)); } - if (typeof num === "string") { - return num; - } - return `${round(num, options?.maximumFractionDigits).toString()}${ - options?.style === "currency" ? ` ${options.currency}` : "" - }`; + } + if (typeof num === "string") { + return num; + } + return `${round(num, options?.maximumFractionDigits).toString()}${ + options?.style === "currency" ? ` ${options.currency}` : "" + }`; }; /** @@ -79,26 +91,30 @@ export const formatNumber = ( * @returns An `Intl.NumberFormatOptions` object with `maximumFractionDigits` set to 0, or `undefined` */ export const getNumberFormatOptions = ( - entityState: HassEntity, - entity?: EntityRegistryDisplayEntry + entityState: HassEntity, + entity?: EntityRegistryDisplayEntry ): Intl.NumberFormatOptions | undefined => { - const precision = entity?.display_precision; - if (precision != null) { - return { - maximumFractionDigits: precision, - minimumFractionDigits: precision, - }; - } - if ( - Number.isInteger(Number(entityState.attributes?.step)) && - Number.isInteger(Number(entityState.state)) - ) { - return { maximumFractionDigits: 0 }; - } - if (entityState.attributes.step != null) { - return { maximumFractionDigits: Math.ceil(Math.log10(1 / entityState.attributes.step)) }; - } - return undefined; + const precision = entity?.display_precision; + if (precision != null) { + return { + maximumFractionDigits: precision, + minimumFractionDigits: precision, + }; + } + if ( + Number.isInteger(Number(entityState.attributes?.step)) && + Number.isInteger(Number(entityState.state)) + ) { + return { maximumFractionDigits: 0 }; + } + if (entityState.attributes.step != null) { + return { + maximumFractionDigits: Math.ceil( + Math.log10(1 / entityState.attributes.step) + ), + }; + } + return undefined; }; /** @@ -107,27 +123,28 @@ export const getNumberFormatOptions = ( * @param options The Intl.NumberFormatOptions that should be included in the returned options */ export const getDefaultFormatOptions = ( - num: string | number, - options?: Intl.NumberFormatOptions + num: string | number, + options?: Intl.NumberFormatOptions ): Intl.NumberFormatOptions => { - const defaultOptions: Intl.NumberFormatOptions = { - maximumFractionDigits: 2, - ...options, - }; + const defaultOptions: Intl.NumberFormatOptions = { + maximumFractionDigits: 2, + ...options, + }; - if (typeof num !== "string") { - return defaultOptions; - } + if (typeof num !== "string") { + return defaultOptions; + } - // Keep decimal trailing zeros if they are present in a string numeric value - if ( - !options || - (options.minimumFractionDigits === undefined && options.maximumFractionDigits === undefined) - ) { - const digits = num.indexOf(".") > -1 ? num.split(".")[1].length : 0; - defaultOptions.minimumFractionDigits = digits; - defaultOptions.maximumFractionDigits = digits; - } + // Keep decimal trailing zeros if they are present in a string numeric value + if ( + !options || + (options.minimumFractionDigits === undefined && + options.maximumFractionDigits === undefined) + ) { + const digits = num.indexOf(".") > -1 ? num.split(".")[1].length : 0; + defaultOptions.minimumFractionDigits = digits; + defaultOptions.maximumFractionDigits = digits; + } - return defaultOptions; + return defaultOptions; }; diff --git a/src/ha/common/number/round.ts b/src/ha/common/number/round.ts index ee84f6b45..d0da48ca0 100644 --- a/src/ha/common/number/round.ts +++ b/src/ha/common/number/round.ts @@ -1,2 +1,2 @@ export const round = (value: number, precision = 2): number => - Math.round(value * 10 ** precision) / 10 ** precision; + Math.round(value * 10 ** precision) / 10 ** precision; diff --git a/src/ha/common/string/compare.ts b/src/ha/common/string/compare.ts index 27ed81a42..a14e087cc 100644 --- a/src/ha/common/string/compare.ts +++ b/src/ha/common/string/compare.ts @@ -1,40 +1,47 @@ import memoizeOne from "memoize-one"; -const collator = memoizeOne((language: string | undefined) => new Intl.Collator(language)); +const collator = memoizeOne( + (language: string | undefined) => new Intl.Collator(language) +); const caseInsensitiveCollator = memoizeOne( - (language: string | undefined) => new Intl.Collator(language, { sensitivity: "accent" }) + (language: string | undefined) => + new Intl.Collator(language, { sensitivity: "accent" }) ); const fallbackStringCompare = (a: string, b: string) => { - if (a < b) { - return -1; - } - if (a > b) { - return 1; - } - - return 0; + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + + return 0; }; -export const stringCompare = (a: string, b: string, language: string | undefined = undefined) => { - // @ts-ignore - if (Intl?.Collator) { - return collator(language).compare(a, b); - } +export const stringCompare = ( + a: string, + b: string, + language: string | undefined = undefined +) => { + // @ts-ignore + if (Intl?.Collator) { + return collator(language).compare(a, b); + } - return fallbackStringCompare(a, b); + return fallbackStringCompare(a, b); }; export const caseInsensitiveStringCompare = ( - a: string, - b: string, - language: string | undefined = undefined + a: string, + b: string, + language: string | undefined = undefined ) => { - // @ts-ignore - if (Intl?.Collator) { - return caseInsensitiveCollator(language).compare(a, b); - } + // @ts-ignore + if (Intl?.Collator) { + return caseInsensitiveCollator(language).compare(a, b); + } - return fallbackStringCompare(a.toLowerCase(), b.toLowerCase()); + return fallbackStringCompare(a.toLowerCase(), b.toLowerCase()); }; diff --git a/src/ha/common/structs/handle-errors.ts b/src/ha/common/structs/handle-errors.ts index 2b0edbd5b..96c82081c 100644 --- a/src/ha/common/structs/handle-errors.ts +++ b/src/ha/common/structs/handle-errors.ts @@ -2,50 +2,58 @@ import { StructError } from "superstruct"; import type { HomeAssistant } from "../../types"; export const handleStructError = ( - hass: HomeAssistant, - err: Error + hass: HomeAssistant, + err: Error ): { warnings: string[]; errors?: string[] } => { - if (!(err instanceof StructError)) { - return { warnings: [err.message], errors: undefined }; + if (!(err instanceof StructError)) { + return { warnings: [err.message], errors: undefined }; + } + const errors: string[] = []; + const warnings: string[] = []; + for (const failure of err.failures()) { + if (failure.value === undefined) { + errors.push( + hass.localize( + "ui.errors.config.key_missing", + "key", + failure.path.join(".") + ) + ); + } else if (failure.type === "never") { + warnings.push( + hass.localize( + "ui.errors.config.key_not_expected", + "key", + failure.path.join(".") + ) + ); + } else if (failure.type === "union") { + continue; + } else if (failure.type === "enums") { + warnings.push( + hass.localize( + "ui.errors.config.key_wrong_type", + "key", + failure.path.join("."), + "type_correct", + failure.message.replace("Expected ", "").split(", ")[0], + "type_wrong", + JSON.stringify(failure.value) + ) + ); + } else { + warnings.push( + hass.localize( + "ui.errors.config.key_wrong_type", + "key", + failure.path.join("."), + "type_correct", + failure.refinement || failure.type, + "type_wrong", + JSON.stringify(failure.value) + ) + ); } - const errors: string[] = []; - const warnings: string[] = []; - for (const failure of err.failures()) { - if (failure.value === undefined) { - errors.push( - hass.localize("ui.errors.config.key_missing", "key", failure.path.join(".")) - ); - } else if (failure.type === "never") { - warnings.push( - hass.localize("ui.errors.config.key_not_expected", "key", failure.path.join(".")) - ); - } else if (failure.type === "union") { - continue; - } else if (failure.type === "enums") { - warnings.push( - hass.localize( - "ui.errors.config.key_wrong_type", - "key", - failure.path.join("."), - "type_correct", - failure.message.replace("Expected ", "").split(", ")[0], - "type_wrong", - JSON.stringify(failure.value) - ) - ); - } else { - warnings.push( - hass.localize( - "ui.errors.config.key_wrong_type", - "key", - failure.path.join("."), - "type_correct", - failure.refinement || failure.type, - "type_wrong", - JSON.stringify(failure.value) - ) - ); - } - } - return { warnings, errors }; + } + return { warnings, errors }; }; diff --git a/src/ha/common/translations/blank_before_percent.ts b/src/ha/common/translations/blank_before_percent.ts index 9c79ead85..4c489c96c 100644 --- a/src/ha/common/translations/blank_before_percent.ts +++ b/src/ha/common/translations/blank_before_percent.ts @@ -1,16 +1,18 @@ import { FrontendLocaleData } from "../../data/translation"; // Logic based on https://en.wikipedia.org/wiki/Percent_sign#Form_and_spacing -export const blankBeforePercent = (localeOptions: FrontendLocaleData): string => { - switch (localeOptions.language) { - case "cz": - case "de": - case "fi": - case "fr": - case "sk": - case "sv": - return " "; - default: - return ""; - } +export const blankBeforePercent = ( + localeOptions: FrontendLocaleData +): string => { + switch (localeOptions.language) { + case "cz": + case "de": + case "fi": + case "fr": + case "sk": + case "sv": + return " "; + default: + return ""; + } }; diff --git a/src/ha/common/util/compute_rtl.ts b/src/ha/common/util/compute_rtl.ts index fd5f24517..7e71b1b15 100644 --- a/src/ha/common/util/compute_rtl.ts +++ b/src/ha/common/util/compute_rtl.ts @@ -2,29 +2,35 @@ import { LitElement } from "lit"; import { HomeAssistant } from "../../types"; export function computeRTL(hass: HomeAssistant) { - const lang = hass.language || "en"; - if (hass.translationMetadata.translations[lang]) { - return hass.translationMetadata.translations[lang].isRTL || false; - } - return false; + const lang = hass.language || "en"; + if (hass.translationMetadata.translations[lang]) { + return hass.translationMetadata.translations[lang].isRTL || false; + } + return false; } export function computeRTLDirection(hass: HomeAssistant) { - return emitRTLDirection(computeRTL(hass)); + return emitRTLDirection(computeRTL(hass)); } export function emitRTLDirection(rtl: boolean) { - return rtl ? "rtl" : "ltr"; + return rtl ? "rtl" : "ltr"; } export function computeDirectionStyles(isRTL: boolean, element: LitElement) { - const direction: string = emitRTLDirection(isRTL); - setDirectionStyles(direction, element); + const direction: string = emitRTLDirection(isRTL); + setDirectionStyles(direction, element); } export function setDirectionStyles(direction: string, element: LitElement) { - element.style.direction = direction; - element.style.setProperty("--direction", direction); - element.style.setProperty("--float-start", direction === "ltr" ? "left" : "right"); - element.style.setProperty("--float-end", direction === "ltr" ? "right" : "left"); + element.style.direction = direction; + element.style.setProperty("--direction", direction); + element.style.setProperty( + "--float-start", + direction === "ltr" ? "left" : "right" + ); + element.style.setProperty( + "--float-end", + direction === "ltr" ? "right" : "left" + ); } diff --git a/src/ha/common/util/debounce.ts b/src/ha/common/util/debounce.ts index bb5acb652..cd61f072b 100644 --- a/src/ha/common/util/debounce.ts +++ b/src/ha/common/util/debounce.ts @@ -6,27 +6,27 @@ // leading edge, instead of the trailing. export const debounce = ( - func: (...args: T) => void, - wait: number, - immediate = false + func: (...args: T) => void, + wait: number, + immediate = false ) => { - let timeout: number | undefined; - const debouncedFunc = (...args: T): void => { - const later = () => { - timeout = undefined; - if (!immediate) { - func(...args); - } - }; - const callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = window.setTimeout(later, wait); - if (callNow) { - func(...args); - } + let timeout: number | undefined; + const debouncedFunc = (...args: T): void => { + const later = () => { + timeout = undefined; + if (!immediate) { + func(...args); + } }; - debouncedFunc.cancel = () => { - clearTimeout(timeout); - }; - return debouncedFunc; + const callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = window.setTimeout(later, wait); + if (callNow) { + func(...args); + } + }; + debouncedFunc.cancel = () => { + clearTimeout(timeout); + }; + return debouncedFunc; }; diff --git a/src/ha/common/util/deep-equal.ts b/src/ha/common/util/deep-equal.ts index a272c04df..780aef695 100644 --- a/src/ha/common/util/deep-equal.ts +++ b/src/ha/common/util/deep-equal.ts @@ -1,107 +1,107 @@ // From https://github.com/epoberezkin/fast-deep-equal // MIT License - Copyright (c) 2017 Evgeny Poberezkin export const deepEqual = (a: any, b: any): boolean => { - if (a === b) { - return true; - } + if (a === b) { + return true; + } - if (a && b && typeof a === "object" && typeof b === "object") { - if (a.constructor !== b.constructor) { - return false; - } + if (a && b && typeof a === "object" && typeof b === "object") { + if (a.constructor !== b.constructor) { + return false; + } - let i: number | [any, any]; - let length: number; - if (Array.isArray(a)) { - length = a.length; - if (length !== b.length) { - return false; - } - for (i = length; i-- !== 0; ) { - if (!deepEqual(a[i], b[i])) { - return false; - } - } - return true; + let i: number | [any, any]; + let length: number; + if (Array.isArray(a)) { + length = a.length; + if (length !== b.length) { + return false; + } + for (i = length; i-- !== 0; ) { + if (!deepEqual(a[i], b[i])) { + return false; } + } + return true; + } - if (a instanceof Map && b instanceof Map) { - if (a.size !== b.size) { - return false; - } - for (i of a.entries()) { - if (!b.has(i[0])) { - return false; - } - } - for (i of a.entries()) { - if (!deepEqual(i[1], b.get(i[0]))) { - return false; - } - } - return true; + if (a instanceof Map && b instanceof Map) { + if (a.size !== b.size) { + return false; + } + for (i of a.entries()) { + if (!b.has(i[0])) { + return false; } - - if (a instanceof Set && b instanceof Set) { - if (a.size !== b.size) { - return false; - } - for (i of a.entries()) { - if (!b.has(i[0])) { - return false; - } - } - return true; + } + for (i of a.entries()) { + if (!deepEqual(i[1], b.get(i[0]))) { + return false; } + } + return true; + } - if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) { - // @ts-ignore - length = a.length; - // @ts-ignore - if (length !== b.length) { - return false; - } - for (i = length; i-- !== 0; ) { - if (a[i] !== b[i]) { - return false; - } - } - return true; + if (a instanceof Set && b instanceof Set) { + if (a.size !== b.size) { + return false; + } + for (i of a.entries()) { + if (!b.has(i[0])) { + return false; } + } + return true; + } - if (a.constructor === RegExp) { - return a.source === b.source && a.flags === b.flags; - } - if (a.valueOf !== Object.prototype.valueOf) { - return a.valueOf() === b.valueOf(); - } - if (a.toString !== Object.prototype.toString) { - return a.toString() === b.toString(); + if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) { + // @ts-ignore + length = a.length; + // @ts-ignore + if (length !== b.length) { + return false; + } + for (i = length; i-- !== 0; ) { + if (a[i] !== b[i]) { + return false; } + } + return true; + } - const keys = Object.keys(a); - length = keys.length; - if (length !== Object.keys(b).length) { - return false; - } - for (i = length; i-- !== 0; ) { - if (!Object.prototype.hasOwnProperty.call(b, keys[i])) { - return false; - } - } + if (a.constructor === RegExp) { + return a.source === b.source && a.flags === b.flags; + } + if (a.valueOf !== Object.prototype.valueOf) { + return a.valueOf() === b.valueOf(); + } + if (a.toString !== Object.prototype.toString) { + return a.toString() === b.toString(); + } - for (i = length; i-- !== 0; ) { - const key = keys[i]; + const keys = Object.keys(a); + length = keys.length; + if (length !== Object.keys(b).length) { + return false; + } + for (i = length; i-- !== 0; ) { + if (!Object.prototype.hasOwnProperty.call(b, keys[i])) { + return false; + } + } - if (!deepEqual(a[key], b[key])) { - return false; - } - } + for (i = length; i-- !== 0; ) { + const key = keys[i]; - return true; + if (!deepEqual(a[key], b[key])) { + return false; + } } - // true if both NaN, false otherwise - // eslint-disable-next-line no-self-compare - return a !== a && b !== b; + return true; + } + + // true if both NaN, false otherwise + // eslint-disable-next-line no-self-compare + return a !== a && b !== b; }; diff --git a/src/ha/common/util/render-status.ts b/src/ha/common/util/render-status.ts index 580c3c992..958d65fc8 100644 --- a/src/ha/common/util/render-status.ts +++ b/src/ha/common/util/render-status.ts @@ -1,8 +1,8 @@ export const afterNextRender = (cb: (value: unknown) => void): void => { - requestAnimationFrame(() => setTimeout(cb, 0)); + requestAnimationFrame(() => setTimeout(cb, 0)); }; export const nextRender = () => - new Promise((resolve) => { - afterNextRender(resolve); - }); + new Promise((resolve) => { + afterNextRender(resolve); + }); diff --git a/src/ha/data/alarm_control_panel.ts b/src/ha/data/alarm_control_panel.ts index bd9dd1a24..fb8109277 100644 --- a/src/ha/data/alarm_control_panel.ts +++ b/src/ha/data/alarm_control_panel.ts @@ -1,112 +1,118 @@ -import { HassEntityAttributeBase, HassEntityBase } from "home-assistant-js-websocket"; +import { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; import { HomeAssistant } from "../types"; import { getExtendedEntityRegistryEntry } from "./entity_registry"; export const enum AlarmControlPanelEntityFeature { - ARM_HOME = 1, - ARM_AWAY = 2, - ARM_NIGHT = 4, - TRIGGER = 8, - ARM_CUSTOM_BYPASS = 16, - ARM_VACATION = 32, + ARM_HOME = 1, + ARM_AWAY = 2, + ARM_NIGHT = 4, + TRIGGER = 8, + ARM_CUSTOM_BYPASS = 16, + ARM_VACATION = 32, } export type AlarmMode = - | "armed_home" - | "armed_away" - | "armed_night" - | "armed_vacation" - | "armed_custom_bypass" - | "disarmed"; + | "armed_home" + | "armed_away" + | "armed_night" + | "armed_vacation" + | "armed_custom_bypass" + | "disarmed"; interface AlarmControlPanelEntityAttributes extends HassEntityAttributeBase { - code_format?: "text" | "number"; - changed_by?: string | null; - code_arm_required?: boolean; + code_format?: "text" | "number"; + changed_by?: string | null; + code_arm_required?: boolean; } export interface AlarmControlPanelEntity extends HassEntityBase { - attributes: AlarmControlPanelEntityAttributes; + attributes: AlarmControlPanelEntityAttributes; } type AlarmConfig = { - service: string; - feature?: AlarmControlPanelEntityFeature; - icon: string; + service: string; + feature?: AlarmControlPanelEntityFeature; + icon: string; }; export const ALARM_MODES: Record = { - armed_home: { - feature: AlarmControlPanelEntityFeature.ARM_HOME, - service: "alarm_arm_home", - icon: "mdi:home", - }, - armed_away: { - feature: AlarmControlPanelEntityFeature.ARM_AWAY, - service: "alarm_arm_away", - icon: "mdi:lock", - }, - armed_night: { - feature: AlarmControlPanelEntityFeature.ARM_NIGHT, - service: "alarm_arm_night", - icon: "mdi:moon-waning-crescent", - }, - armed_vacation: { - feature: AlarmControlPanelEntityFeature.ARM_VACATION, - service: "alarm_arm_vacation", - icon: "mdi:airplane", - }, - armed_custom_bypass: { - feature: AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, - service: "alarm_arm_custom_bypass", - icon: "mdi:shield", - }, - disarmed: { - service: "alarm_disarm", - icon: "mdi:shield-off", - }, + armed_home: { + feature: AlarmControlPanelEntityFeature.ARM_HOME, + service: "alarm_arm_home", + icon: "mdi:home", + }, + armed_away: { + feature: AlarmControlPanelEntityFeature.ARM_AWAY, + service: "alarm_arm_away", + icon: "mdi:lock", + }, + armed_night: { + feature: AlarmControlPanelEntityFeature.ARM_NIGHT, + service: "alarm_arm_night", + icon: "mdi:moon-waning-crescent", + }, + armed_vacation: { + feature: AlarmControlPanelEntityFeature.ARM_VACATION, + service: "alarm_arm_vacation", + icon: "mdi:airplane", + }, + armed_custom_bypass: { + feature: AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, + service: "alarm_arm_custom_bypass", + icon: "mdi:shield", + }, + disarmed: { + service: "alarm_disarm", + icon: "mdi:shield-off", + }, }; export const setProtectedAlarmControlPanelMode = async ( - element: HTMLElement, - hass: HomeAssistant, - stateObj: AlarmControlPanelEntity, - mode: AlarmMode + element: HTMLElement, + hass: HomeAssistant, + stateObj: AlarmControlPanelEntity, + mode: AlarmMode ) => { - const { service } = ALARM_MODES[mode]; + const { service } = ALARM_MODES[mode]; - let code: string | undefined; + let code: string | undefined; - if ( - (mode !== "disarmed" && stateObj.attributes.code_arm_required) || - (mode === "disarmed" && stateObj.attributes.code_format) - ) { - const entry = await getExtendedEntityRegistryEntry(hass, stateObj.entity_id).catch( - () => undefined - ); - const defaultCode = entry?.options?.alarm_control_panel?.default_code; + if ( + (mode !== "disarmed" && stateObj.attributes.code_arm_required) || + (mode === "disarmed" && stateObj.attributes.code_format) + ) { + const entry = await getExtendedEntityRegistryEntry( + hass, + stateObj.entity_id + ).catch(() => undefined); + const defaultCode = entry?.options?.alarm_control_panel?.default_code; - if (!defaultCode) { - const disarm = mode === "disarmed"; + if (!defaultCode) { + const disarm = mode === "disarmed"; - const helpers = await (window as any).loadCardHelpers(); + const helpers = await (window as any).loadCardHelpers(); - const response = await helpers.showEnterCodeDialog(element, { - codeFormat: stateObj.attributes.code_format, - title: hass.localize(`ui.card.alarm_control_panel.${disarm ? "disarm" : "arm"}`), - submitText: hass.localize( - `ui.card.alarm_control_panel.${disarm ? "disarm" : "arm"}` - ), - }); - if (response == null) { - throw new Error("Code dialog closed"); - } - code = response; - } + const response = await helpers.showEnterCodeDialog(element, { + codeFormat: stateObj.attributes.code_format, + title: hass.localize( + `ui.card.alarm_control_panel.${disarm ? "disarm" : "arm"}` + ), + submitText: hass.localize( + `ui.card.alarm_control_panel.${disarm ? "disarm" : "arm"}` + ), + }); + if (response == null) { + throw new Error("Code dialog closed"); + } + code = response; } + } - await hass.callService("alarm_control_panel", service, { - entity_id: stateObj.entity_id, - code, - }); + await hass.callService("alarm_control_panel", service, { + entity_id: stateObj.entity_id, + code, + }); }; diff --git a/src/ha/data/climate.ts b/src/ha/data/climate.ts index bff0bfd61..a6fbea3ba 100644 --- a/src/ha/data/climate.ts +++ b/src/ha/data/climate.ts @@ -1,37 +1,47 @@ -import { HassEntityAttributeBase, HassEntityBase } from "home-assistant-js-websocket"; +import { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; -export type HvacMode = "off" | "heat" | "cool" | "heat_cool" | "auto" | "dry" | "fan_only"; +export type HvacMode = + | "off" + | "heat" + | "cool" + | "heat_cool" + | "auto" + | "dry" + | "fan_only"; export const CLIMATE_PRESET_NONE = "none"; export type HvacAction = "off" | "heating" | "cooling" | "drying" | "idle"; export type ClimateEntity = HassEntityBase & { - attributes: HassEntityAttributeBase & { - hvac_mode: HvacMode; - hvac_modes: HvacMode[]; - hvac_action?: HvacAction; - current_temperature: number; - min_temp: number; - max_temp: number; - temperature: number; - target_temp_step?: number; - target_temp_high?: number; - target_temp_low?: number; - humidity?: number; - current_humidity?: number; - target_humidity_low?: number; - target_humidity_high?: number; - min_humidity?: number; - max_humidity?: number; - fan_mode?: string; - fan_modes?: string[]; - preset_mode?: string; - preset_modes?: string[]; - swing_mode?: string; - swing_modes?: string[]; - aux_heat?: "on" | "off"; - }; + attributes: HassEntityAttributeBase & { + hvac_mode: HvacMode; + hvac_modes: HvacMode[]; + hvac_action?: HvacAction; + current_temperature: number; + min_temp: number; + max_temp: number; + temperature: number; + target_temp_step?: number; + target_temp_high?: number; + target_temp_low?: number; + humidity?: number; + current_humidity?: number; + target_humidity_low?: number; + target_humidity_high?: number; + min_humidity?: number; + max_humidity?: number; + fan_mode?: string; + fan_modes?: string[]; + preset_mode?: string; + preset_modes?: string[]; + swing_mode?: string; + swing_modes?: string[]; + aux_heat?: "on" | "off"; + }; }; export const CLIMATE_SUPPORT_TARGET_TEMPERATURE = 1; @@ -43,14 +53,14 @@ export const CLIMATE_SUPPORT_SWING_MODE = 32; export const CLIMATE_SUPPORT_AUX_HEAT = 64; const hvacModeOrdering: { [key in HvacMode]: number } = { - auto: 1, - heat_cool: 2, - heat: 3, - cool: 4, - dry: 5, - fan_only: 6, - off: 7, + auto: 1, + heat_cool: 2, + heat: 3, + cool: 4, + dry: 5, + fan_only: 6, + off: 7, }; export const compareClimateHvacModes = (mode1: HvacMode, mode2: HvacMode) => - hvacModeOrdering[mode1] - hvacModeOrdering[mode2]; + hvacModeOrdering[mode1] - hvacModeOrdering[mode2]; diff --git a/src/ha/data/cover.ts b/src/ha/data/cover.ts index afa1729ce..a418a1ea2 100644 --- a/src/ha/data/cover.ts +++ b/src/ha/data/cover.ts @@ -1,4 +1,7 @@ -import { HassEntityAttributeBase, HassEntityBase } from "home-assistant-js-websocket"; +import { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; import { supportsFeature } from "../common/entity/supports-feature"; export const COVER_SUPPORT_OPEN = 1; @@ -11,52 +14,52 @@ export const COVER_SUPPORT_STOP_TILT = 64; export const COVER_SUPPORT_SET_TILT_POSITION = 128; export function isFullyOpen(stateObj: CoverEntity) { - if (stateObj.attributes.current_position !== undefined) { - return stateObj.attributes.current_position === 100; - } - return stateObj.state === "open"; + if (stateObj.attributes.current_position !== undefined) { + return stateObj.attributes.current_position === 100; + } + return stateObj.state === "open"; } export function isFullyClosed(stateObj: CoverEntity) { - if (stateObj.attributes.current_position !== undefined) { - return stateObj.attributes.current_position === 0; - } - return stateObj.state === "closed"; + if (stateObj.attributes.current_position !== undefined) { + return stateObj.attributes.current_position === 0; + } + return stateObj.state === "closed"; } export function isFullyOpenTilt(stateObj: CoverEntity) { - return stateObj.attributes.current_tilt_position === 100; + return stateObj.attributes.current_tilt_position === 100; } export function isFullyClosedTilt(stateObj: CoverEntity) { - return stateObj.attributes.current_tilt_position === 0; + return stateObj.attributes.current_tilt_position === 0; } export function isOpening(stateObj: CoverEntity) { - return stateObj.state === "opening"; + return stateObj.state === "opening"; } export function isClosing(stateObj: CoverEntity) { - return stateObj.state === "closing"; + return stateObj.state === "closing"; } export function isTiltOnly(stateObj: CoverEntity) { - const supportsCover = - supportsFeature(stateObj, COVER_SUPPORT_OPEN) || - supportsFeature(stateObj, COVER_SUPPORT_CLOSE) || - supportsFeature(stateObj, COVER_SUPPORT_STOP); - const supportsTilt = - supportsFeature(stateObj, COVER_SUPPORT_OPEN_TILT) || - supportsFeature(stateObj, COVER_SUPPORT_CLOSE_TILT) || - supportsFeature(stateObj, COVER_SUPPORT_STOP_TILT); - return supportsTilt && !supportsCover; + const supportsCover = + supportsFeature(stateObj, COVER_SUPPORT_OPEN) || + supportsFeature(stateObj, COVER_SUPPORT_CLOSE) || + supportsFeature(stateObj, COVER_SUPPORT_STOP); + const supportsTilt = + supportsFeature(stateObj, COVER_SUPPORT_OPEN_TILT) || + supportsFeature(stateObj, COVER_SUPPORT_CLOSE_TILT) || + supportsFeature(stateObj, COVER_SUPPORT_STOP_TILT); + return supportsTilt && !supportsCover; } interface CoverEntityAttributes extends HassEntityAttributeBase { - current_position: number; - current_tilt_position: number; + current_position: number; + current_tilt_position: number; } export interface CoverEntity extends HassEntityBase { - attributes: CoverEntityAttributes; + attributes: CoverEntityAttributes; } diff --git a/src/ha/data/entity.ts b/src/ha/data/entity.ts index 2eee6acb6..bcd91d480 100644 --- a/src/ha/data/entity.ts +++ b/src/ha/data/entity.ts @@ -10,51 +10,51 @@ export const OFF = "off"; const OFF_STATES = [UNAVAILABLE, UNKNOWN, OFF]; export function isActive(stateObj: HassEntity) { - const domain = computeDomain(stateObj.entity_id); - const state = stateObj.state; - - if (["button", "input_button", "scene"].includes(domain)) { - return state !== UNAVAILABLE; - } - - if (OFF_STATES.includes(state)) { - return false; - } - - // Custom cases - switch (domain) { - case "cover": - case "valve": - return !["closed", "closing"].includes(state); - case "device_tracker": - case "person": - return state !== "not_home"; - case "media_player": - return state !== "standby"; - case "vacuum": - return !["idle", "docked", "paused"].includes(state); - case "plant": - return state === "problem"; - default: - return true; - } + const domain = computeDomain(stateObj.entity_id); + const state = stateObj.state; + + if (["button", "input_button", "scene"].includes(domain)) { + return state !== UNAVAILABLE; + } + + if (OFF_STATES.includes(state)) { + return false; + } + + // Custom cases + switch (domain) { + case "cover": + case "valve": + return !["closed", "closing"].includes(state); + case "device_tracker": + case "person": + return state !== "not_home"; + case "media_player": + return state !== "standby"; + case "vacuum": + return !["idle", "docked", "paused"].includes(state); + case "plant": + return state === "problem"; + default: + return true; + } } export function isAvailable(stateObj: HassEntity) { - return stateObj.state !== UNAVAILABLE; + return stateObj.state !== UNAVAILABLE; } export function isOff(stateObj: HassEntity) { - return stateObj.state === OFF; + return stateObj.state === OFF; } export function isUnknown(stateObj: HassEntity) { - return stateObj.state === UNKNOWN; + return stateObj.state === UNKNOWN; } export function getEntityPicture(stateObj: HassEntity) { - return ( - (stateObj.attributes.entity_picture_local as string | undefined) || - stateObj.attributes.entity_picture - ); + return ( + (stateObj.attributes.entity_picture_local as string | undefined) || + stateObj.attributes.entity_picture + ); } diff --git a/src/ha/data/entity_registry.ts b/src/ha/data/entity_registry.ts index 3b6ce5c32..97ae7f428 100644 --- a/src/ha/data/entity_registry.ts +++ b/src/ha/data/entity_registry.ts @@ -10,287 +10,306 @@ import { LightColor } from "./light"; type entityCategory = "config" | "diagnostic"; export interface EntityRegistryDisplayEntry { - entity_id: string; - name?: string; - device_id?: string; - area_id?: string; - hidden?: boolean; - entity_category?: entityCategory; - translation_key?: string; - platform?: string; - display_precision?: number; + entity_id: string; + name?: string; + device_id?: string; + area_id?: string; + hidden?: boolean; + entity_category?: entityCategory; + translation_key?: string; + platform?: string; + display_precision?: number; } interface EntityRegistryDisplayEntryResponse { - entities: { - ei: string; - di?: string; - ai?: string; - ec?: number; - en?: string; - pl?: string; - tk?: string; - hb?: boolean; - dp?: number; - }[]; - entity_categories: Record; + entities: { + ei: string; + di?: string; + ai?: string; + ec?: number; + en?: string; + pl?: string; + tk?: string; + hb?: boolean; + dp?: number; + }[]; + entity_categories: Record; } export interface EntityRegistryEntry { - id: string; - entity_id: string; - name: string | null; - icon: string | null; - platform: string; - config_entry_id: string | null; - device_id: string | null; - area_id: string | null; - disabled_by: "user" | "device" | "integration" | "config_entry" | null; - hidden_by: Exclude; - entity_category: entityCategory | null; - has_entity_name: boolean; - original_name?: string; - unique_id: string; - translation_key?: string; - options: EntityRegistryOptions | null; + id: string; + entity_id: string; + name: string | null; + icon: string | null; + platform: string; + config_entry_id: string | null; + device_id: string | null; + area_id: string | null; + disabled_by: "user" | "device" | "integration" | "config_entry" | null; + hidden_by: Exclude; + entity_category: entityCategory | null; + has_entity_name: boolean; + original_name?: string; + unique_id: string; + translation_key?: string; + options: EntityRegistryOptions | null; } export interface ExtEntityRegistryEntry extends EntityRegistryEntry { - capabilities: Record; - original_icon?: string; - device_class?: string; - original_device_class?: string; - aliases: string[]; + capabilities: Record; + original_icon?: string; + device_class?: string; + original_device_class?: string; + aliases: string[]; } export interface UpdateEntityRegistryEntryResult { - entity_entry: ExtEntityRegistryEntry; - reload_delay?: number; - require_restart?: boolean; + entity_entry: ExtEntityRegistryEntry; + reload_delay?: number; + require_restart?: boolean; } export interface SensorEntityOptions { - display_precision?: number | null; - suggested_display_precision?: number | null; - unit_of_measurement?: string | null; + display_precision?: number | null; + suggested_display_precision?: number | null; + unit_of_measurement?: string | null; } export interface LightEntityOptions { - favorite_colors?: LightColor[]; + favorite_colors?: LightColor[]; } export interface NumberEntityOptions { - unit_of_measurement?: string | null; + unit_of_measurement?: string | null; } export interface LockEntityOptions { - default_code?: string | null; + default_code?: string | null; } export interface WeatherEntityOptions { - precipitation_unit?: string | null; - pressure_unit?: string | null; - temperature_unit?: string | null; - visibility_unit?: string | null; - wind_speed_unit?: string | null; + precipitation_unit?: string | null; + pressure_unit?: string | null; + temperature_unit?: string | null; + visibility_unit?: string | null; + wind_speed_unit?: string | null; } export interface SwitchAsXEntityOptions { - entity_id: string; + entity_id: string; } export interface AlarmControlPanelEntityOptions { - default_code?: string | null; + default_code?: string | null; } export interface EntityRegistryOptions { - number?: NumberEntityOptions; - sensor?: SensorEntityOptions; - alarm_control_panel?: AlarmControlPanelEntityOptions; - lock?: LockEntityOptions; - weather?: WeatherEntityOptions; - light?: LightEntityOptions; - switch_as_x?: SwitchAsXEntityOptions; - conversation?: Record; - "cloud.alexa"?: Record; - "cloud.google_assistant"?: Record; + number?: NumberEntityOptions; + sensor?: SensorEntityOptions; + alarm_control_panel?: AlarmControlPanelEntityOptions; + lock?: LockEntityOptions; + weather?: WeatherEntityOptions; + light?: LightEntityOptions; + switch_as_x?: SwitchAsXEntityOptions; + conversation?: Record; + "cloud.alexa"?: Record; + "cloud.google_assistant"?: Record; } export interface EntityRegistryEntryUpdateParams { - name?: string | null; - icon?: string | null; - device_class?: string | null; - area_id?: string | null; - disabled_by?: string | null; - hidden_by: string | null; - new_entity_id?: string; - options_domain?: string; - options?: - | SensorEntityOptions - | NumberEntityOptions - | LockEntityOptions - | WeatherEntityOptions - | LightEntityOptions; - aliases?: string[]; + name?: string | null; + icon?: string | null; + device_class?: string | null; + area_id?: string | null; + disabled_by?: string | null; + hidden_by: string | null; + new_entity_id?: string; + options_domain?: string; + options?: + | SensorEntityOptions + | NumberEntityOptions + | LockEntityOptions + | WeatherEntityOptions + | LightEntityOptions; + aliases?: string[]; } export const findBatteryEntity = ( - hass: HomeAssistant, - entities: EntityRegistryEntry[] + hass: HomeAssistant, + entities: EntityRegistryEntry[] ): EntityRegistryEntry | undefined => - entities.find( - (entity) => - hass.states[entity.entity_id] && - hass.states[entity.entity_id].attributes.device_class === "battery" - ); + entities.find( + (entity) => + hass.states[entity.entity_id] && + hass.states[entity.entity_id].attributes.device_class === "battery" + ); export const findBatteryChargingEntity = ( - hass: HomeAssistant, - entities: EntityRegistryEntry[] + hass: HomeAssistant, + entities: EntityRegistryEntry[] ): EntityRegistryEntry | undefined => - entities.find( - (entity) => - hass.states[entity.entity_id] && - hass.states[entity.entity_id].attributes.device_class === "battery_charging" - ); + entities.find( + (entity) => + hass.states[entity.entity_id] && + hass.states[entity.entity_id].attributes.device_class === + "battery_charging" + ); export const computeEntityRegistryName = ( - hass: HomeAssistant, - entry: EntityRegistryEntry + hass: HomeAssistant, + entry: EntityRegistryEntry ): string | null => { - if (entry.name) { - return entry.name; - } - const state = hass.states[entry.entity_id]; - if (state) { - return computeStateName(state); - } - return entry.original_name ? entry.original_name : entry.entity_id; + if (entry.name) { + return entry.name; + } + const state = hass.states[entry.entity_id]; + if (state) { + return computeStateName(state); + } + return entry.original_name ? entry.original_name : entry.entity_id; }; export const getExtendedEntityRegistryEntry = ( - hass: HomeAssistant, - entityId: string + hass: HomeAssistant, + entityId: string ): Promise => - hass.callWS({ - type: "config/entity_registry/get", - entity_id: entityId, - }); + hass.callWS({ + type: "config/entity_registry/get", + entity_id: entityId, + }); export const getExtendedEntityRegistryEntries = ( - hass: HomeAssistant, - entityIds: string[] + hass: HomeAssistant, + entityIds: string[] ): Promise> => - hass.callWS({ - type: "config/entity_registry/get_entries", - entity_ids: entityIds, - }); + hass.callWS({ + type: "config/entity_registry/get_entries", + entity_ids: entityIds, + }); export const updateEntityRegistryEntry = ( - hass: HomeAssistant, - entityId: string, - updates: Partial + hass: HomeAssistant, + entityId: string, + updates: Partial ): Promise => - hass.callWS({ - type: "config/entity_registry/update", - entity_id: entityId, - ...updates, - }); - -export const removeEntityRegistryEntry = (hass: HomeAssistant, entityId: string): Promise => - hass.callWS({ - type: "config/entity_registry/remove", - entity_id: entityId, - }); + hass.callWS({ + type: "config/entity_registry/update", + entity_id: entityId, + ...updates, + }); + +export const removeEntityRegistryEntry = ( + hass: HomeAssistant, + entityId: string +): Promise => + hass.callWS({ + type: "config/entity_registry/remove", + entity_id: entityId, + }); export const fetchEntityRegistry = (conn: Connection) => - conn.sendMessagePromise({ - type: "config/entity_registry/list", - }); + conn.sendMessagePromise({ + type: "config/entity_registry/list", + }); export const fetchEntityRegistryDisplay = (conn: Connection) => - conn.sendMessagePromise({ - type: "config/entity_registry/list_for_display", - }); - -const subscribeEntityRegistryUpdates = (conn: Connection, store: Store) => - conn.subscribeEvents( - debounce( - () => fetchEntityRegistry(conn).then((entities) => store.setState(entities, true)), - 500, - true + conn.sendMessagePromise({ + type: "config/entity_registry/list_for_display", + }); + +const subscribeEntityRegistryUpdates = ( + conn: Connection, + store: Store +) => + conn.subscribeEvents( + debounce( + () => + fetchEntityRegistry(conn).then((entities) => + store.setState(entities, true) ), - "entity_registry_updated" - ); + 500, + true + ), + "entity_registry_updated" + ); export const subscribeEntityRegistry = ( - conn: Connection, - onChange: (entities: EntityRegistryEntry[]) => void + conn: Connection, + onChange: (entities: EntityRegistryEntry[]) => void ) => - createCollection( - "_entityRegistry", - fetchEntityRegistry, - subscribeEntityRegistryUpdates, - conn, - onChange - ); + createCollection( + "_entityRegistry", + fetchEntityRegistry, + subscribeEntityRegistryUpdates, + conn, + onChange + ); const subscribeEntityRegistryDisplayUpdates = ( - conn: Connection, - store: Store + conn: Connection, + store: Store ) => - conn.subscribeEvents( - debounce( - () => - fetchEntityRegistryDisplay(conn).then((entities) => store.setState(entities, true)), - 500, - true + conn.subscribeEvents( + debounce( + () => + fetchEntityRegistryDisplay(conn).then((entities) => + store.setState(entities, true) ), - "entity_registry_updated" - ); + 500, + true + ), + "entity_registry_updated" + ); export const subscribeEntityRegistryDisplay = ( - conn: Connection, - onChange: (entities: EntityRegistryDisplayEntryResponse) => void + conn: Connection, + onChange: (entities: EntityRegistryDisplayEntryResponse) => void +) => + createCollection( + "_entityRegistryDisplay", + fetchEntityRegistryDisplay, + subscribeEntityRegistryDisplayUpdates, + conn, + onChange + ); + +export const sortEntityRegistryByName = ( + entries: EntityRegistryEntry[], + language: string ) => - createCollection( - "_entityRegistryDisplay", - fetchEntityRegistryDisplay, - subscribeEntityRegistryDisplayUpdates, - conn, - onChange - ); - -export const sortEntityRegistryByName = (entries: EntityRegistryEntry[], language: string) => - entries.sort((entry1, entry2) => - caseInsensitiveStringCompare(entry1.name || "", entry2.name || "", language) - ); - -export const entityRegistryByEntityId = memoizeOne((entries: EntityRegistryEntry[]) => { + entries.sort((entry1, entry2) => + caseInsensitiveStringCompare(entry1.name || "", entry2.name || "", language) + ); + +export const entityRegistryByEntityId = memoizeOne( + (entries: EntityRegistryEntry[]) => { const entities: Record = {}; for (const entity of entries) { - entities[entity.entity_id] = entity; + entities[entity.entity_id] = entity; } return entities; -}); + } +); -export const entityRegistryById = memoizeOne((entries: EntityRegistryEntry[]) => { +export const entityRegistryById = memoizeOne( + (entries: EntityRegistryEntry[]) => { const entities: Record = {}; for (const entity of entries) { - entities[entity.id] = entity; + entities[entity.id] = entity; } return entities; -}); + } +); export const getEntityPlatformLookup = ( - entities: EntityRegistryEntry[] + entities: EntityRegistryEntry[] ): Record => { - const entityLookup = {}; - for (const confEnt of entities) { - if (!confEnt.platform) { - continue; - } - entityLookup[confEnt.entity_id] = confEnt.platform; + const entityLookup = {}; + for (const confEnt of entities) { + if (!confEnt.platform) { + continue; } - return entityLookup; + entityLookup[confEnt.entity_id] = confEnt.platform; + } + return entityLookup; }; diff --git a/src/ha/data/humidifier.ts b/src/ha/data/humidifier.ts index 962067800..2cc661c7d 100644 --- a/src/ha/data/humidifier.ts +++ b/src/ha/data/humidifier.ts @@ -1,13 +1,16 @@ -import { HassEntityAttributeBase, HassEntityBase } from "home-assistant-js-websocket"; +import { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; export type HumidifierEntity = HassEntityBase & { - attributes: HassEntityAttributeBase & { - humidity?: number; - min_humidity?: number; - max_humidity?: number; - mode?: string; - available_modes?: string[]; - }; + attributes: HassEntityAttributeBase & { + humidity?: number; + min_humidity?: number; + max_humidity?: number; + mode?: string; + available_modes?: string[]; + }; }; export const HUMIDIFIER_SUPPORT_MODES = 1; diff --git a/src/ha/data/light.ts b/src/ha/data/light.ts index a1ea97d68..661d703d1 100644 --- a/src/ha/data/light.ts +++ b/src/ha/data/light.ts @@ -1,101 +1,111 @@ -import { HassEntityAttributeBase, HassEntityBase } from "home-assistant-js-websocket"; +import { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; export const enum LightEntityFeature { - EFFECT = 4, - FLASH = 8, - TRANSITION = 32, + EFFECT = 4, + FLASH = 8, + TRANSITION = 32, } export const enum LightColorMode { - UNKNOWN = "unknown", - ONOFF = "onoff", - BRIGHTNESS = "brightness", - COLOR_TEMP = "color_temp", - HS = "hs", - XY = "xy", - RGB = "rgb", - RGBW = "rgbw", - RGBWW = "rgbww", - WHITE = "white", + UNKNOWN = "unknown", + ONOFF = "onoff", + BRIGHTNESS = "brightness", + COLOR_TEMP = "color_temp", + HS = "hs", + XY = "xy", + RGB = "rgb", + RGBW = "rgbw", + RGBWW = "rgbww", + WHITE = "white", } const modesSupportingColor = [ - LightColorMode.HS, - LightColorMode.XY, - LightColorMode.RGB, - LightColorMode.RGBW, - LightColorMode.RGBWW, + LightColorMode.HS, + LightColorMode.XY, + LightColorMode.RGB, + LightColorMode.RGBW, + LightColorMode.RGBWW, ]; const modesSupportingBrightness = [ - ...modesSupportingColor, - LightColorMode.COLOR_TEMP, - LightColorMode.BRIGHTNESS, - LightColorMode.WHITE, + ...modesSupportingColor, + LightColorMode.COLOR_TEMP, + LightColorMode.BRIGHTNESS, + LightColorMode.WHITE, ]; -export const lightSupportsColorMode = (entity: LightEntity, mode: LightColorMode) => - entity.attributes.supported_color_modes?.includes(mode) || false; +export const lightSupportsColorMode = ( + entity: LightEntity, + mode: LightColorMode +) => entity.attributes.supported_color_modes?.includes(mode) || false; export const lightIsInColorMode = (entity: LightEntity) => - (entity.attributes.color_mode && modesSupportingColor.includes(entity.attributes.color_mode)) || - false; + (entity.attributes.color_mode && + modesSupportingColor.includes(entity.attributes.color_mode)) || + false; export const lightSupportsColor = (entity: LightEntity) => - entity.attributes.supported_color_modes?.some((mode) => modesSupportingColor.includes(mode)) || - false; + entity.attributes.supported_color_modes?.some((mode) => + modesSupportingColor.includes(mode) + ) || false; export const lightSupportsBrightness = (entity: LightEntity) => - entity.attributes.supported_color_modes?.some((mode) => - modesSupportingBrightness.includes(mode) - ) || false; + entity.attributes.supported_color_modes?.some((mode) => + modesSupportingBrightness.includes(mode) + ) || false; export const lightSupportsFavoriteColors = (entity: LightEntity) => - lightSupportsColor(entity) || lightSupportsColorMode(entity, LightColorMode.COLOR_TEMP); + lightSupportsColor(entity) || + lightSupportsColorMode(entity, LightColorMode.COLOR_TEMP); -export const getLightCurrentModeRgbColor = (entity: LightEntity): number[] | undefined => - entity.attributes.color_mode === LightColorMode.RGBWW - ? entity.attributes.rgbww_color - : entity.attributes.color_mode === LightColorMode.RGBW - ? entity.attributes.rgbw_color - : entity.attributes.rgb_color; +export const getLightCurrentModeRgbColor = ( + entity: LightEntity +): number[] | undefined => + entity.attributes.color_mode === LightColorMode.RGBWW + ? entity.attributes.rgbww_color + : entity.attributes.color_mode === LightColorMode.RGBW + ? entity.attributes.rgbw_color + : entity.attributes.rgb_color; interface LightEntityAttributes extends HassEntityAttributeBase { - min_color_temp_kelvin?: number; - max_color_temp_kelvin?: number; - min_mireds?: number; - max_mireds?: number; - brightness?: number; - xy_color?: [number, number]; - hs_color?: [number, number]; - color_temp?: number; - color_temp_kelvin?: number; - rgb_color?: [number, number, number]; - rgbw_color?: [number, number, number, number]; - rgbww_color?: [number, number, number, number, number]; - effect?: string; - effect_list?: string[] | null; - supported_color_modes?: LightColorMode[]; - color_mode?: LightColorMode; + min_color_temp_kelvin?: number; + max_color_temp_kelvin?: number; + min_mireds?: number; + max_mireds?: number; + brightness?: number; + xy_color?: [number, number]; + hs_color?: [number, number]; + color_temp?: number; + color_temp_kelvin?: number; + rgb_color?: [number, number, number]; + rgbw_color?: [number, number, number, number]; + rgbww_color?: [number, number, number, number, number]; + effect?: string; + effect_list?: string[] | null; + supported_color_modes?: LightColorMode[]; + color_mode?: LightColorMode; } export interface LightEntity extends HassEntityBase { - attributes: LightEntityAttributes; + attributes: LightEntityAttributes; } export type LightColor = - | { - color_temp_kelvin: number; - } - | { - hs_color: [number, number]; - } - | { - rgb_color: [number, number, number]; - } - | { - rgbw_color: [number, number, number, number]; - } - | { - rgbww_color: [number, number, number, number, number]; - }; + | { + color_temp_kelvin: number; + } + | { + hs_color: [number, number]; + } + | { + rgb_color: [number, number, number]; + } + | { + rgbw_color: [number, number, number, number]; + } + | { + rgbww_color: [number, number, number, number, number]; + }; diff --git a/src/ha/data/lock.ts b/src/ha/data/lock.ts index 921ad159c..e4c3c75d4 100644 --- a/src/ha/data/lock.ts +++ b/src/ha/data/lock.ts @@ -1,29 +1,32 @@ -import { HassEntityAttributeBase, HassEntityBase } from "home-assistant-js-websocket"; +import { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; interface LockEntityAttributes extends HassEntityAttributeBase { - changed_by?: string; - code_format?: string; - code?: string; - is_locked?: boolean; - is_locking?: boolean; - is_unlocking?: boolean; + changed_by?: string; + code_format?: string; + code?: string; + is_locked?: boolean; + is_locking?: boolean; + is_unlocking?: boolean; } export type LockCommand = - | typeof LOCK_SERVICE_LOCK - | typeof LOCK_SERVICE_OPEN - | typeof LOCK_SERVICE_UNLOCK; + | typeof LOCK_SERVICE_LOCK + | typeof LOCK_SERVICE_OPEN + | typeof LOCK_SERVICE_UNLOCK; export type LOCK_STATES = - | typeof LOCK_STATE_JAMMED - | typeof LOCK_STATE_LOCKED - | typeof LOCK_STATE_LOCKING - | typeof LOCK_STATE_UNLOCKED - | typeof LOCK_STATE_UNLOCKING; + | typeof LOCK_STATE_JAMMED + | typeof LOCK_STATE_LOCKED + | typeof LOCK_STATE_LOCKING + | typeof LOCK_STATE_UNLOCKED + | typeof LOCK_STATE_UNLOCKING; export interface LockEntity extends HassEntityBase { - attributes: LockEntityAttributes; - state: LOCK_STATES | "unavailable" | "unknown"; + attributes: LockEntityAttributes; + state: LOCK_STATES | "unavailable" | "unknown"; } export const LOCK_STATE_JAMMED = "jammed"; diff --git a/src/ha/data/lovelace.ts b/src/ha/data/lovelace.ts index 03cf07797..1db60edff 100644 --- a/src/ha/data/lovelace.ts +++ b/src/ha/data/lovelace.ts @@ -1,336 +1,362 @@ import { - Connection, - getCollection, - HassEventBase, - HassServiceTarget, + Connection, + getCollection, + HassEventBase, + HassServiceTarget, } from "home-assistant-js-websocket"; import { HASSDomEvent } from "../common/dom/fire_event"; import { Lovelace, LovelaceCard } from "../panels/lovelace/types"; import { HomeAssistant } from "../types"; export interface LovelacePanelConfig { - mode: "yaml" | "storage"; + mode: "yaml" | "storage"; } export interface LovelaceConfig { - title?: string; - strategy?: { - type: string; - options?: Record; - }; - views: LovelaceViewConfig[]; - background?: string; + title?: string; + strategy?: { + type: string; + options?: Record; + }; + views: LovelaceViewConfig[]; + background?: string; } export interface LegacyLovelaceConfig extends LovelaceConfig { - resources?: LovelaceResource[]; + resources?: LovelaceResource[]; } export interface LovelaceResource { - id: string; - type: "css" | "js" | "module" | "html"; - url: string; + id: string; + type: "css" | "js" | "module" | "html"; + url: string; } export interface LovelaceResourcesMutableParams { - res_type: LovelaceResource["type"]; - url: string; + res_type: LovelaceResource["type"]; + url: string; } -export type LovelaceDashboard = LovelaceYamlDashboard | LovelaceStorageDashboard; +export type LovelaceDashboard = + | LovelaceYamlDashboard + | LovelaceStorageDashboard; interface LovelaceGenericDashboard { - id: string; - url_path: string; - require_admin: boolean; - show_in_sidebar: boolean; - icon?: string; - title: string; + id: string; + url_path: string; + require_admin: boolean; + show_in_sidebar: boolean; + icon?: string; + title: string; } export interface LovelaceYamlDashboard extends LovelaceGenericDashboard { - mode: "yaml"; - filename: string; + mode: "yaml"; + filename: string; } export interface LovelaceStorageDashboard extends LovelaceGenericDashboard { - mode: "storage"; + mode: "storage"; } export interface LovelaceDashboardMutableParams { - require_admin: boolean; - show_in_sidebar: boolean; - icon?: string; - title: string; + require_admin: boolean; + show_in_sidebar: boolean; + icon?: string; + title: string; } -export interface LovelaceDashboardCreateParams extends LovelaceDashboardMutableParams { - url_path: string; - mode: "storage"; +export interface LovelaceDashboardCreateParams + extends LovelaceDashboardMutableParams { + url_path: string; + mode: "storage"; } export interface LovelaceViewConfig { - index?: number; - title?: string; - type?: string; - strategy?: { - type: string; - options?: Record; - }; - cards?: LovelaceCardConfig[]; - path?: string; - icon?: string; - theme?: string; - panel?: boolean; - background?: string; - visible?: boolean | ShowViewConfig[]; + index?: number; + title?: string; + type?: string; + strategy?: { + type: string; + options?: Record; + }; + cards?: LovelaceCardConfig[]; + path?: string; + icon?: string; + theme?: string; + panel?: boolean; + background?: string; + visible?: boolean | ShowViewConfig[]; } export interface LovelaceViewElement extends HTMLElement { - hass?: HomeAssistant; - lovelace?: Lovelace; - narrow?: boolean; - index?: number; - cards?: Array; - isStrategy: boolean; - setConfig(config: LovelaceViewConfig): void; + hass?: HomeAssistant; + lovelace?: Lovelace; + narrow?: boolean; + index?: number; + cards?: Array; + isStrategy: boolean; + setConfig(config: LovelaceViewConfig): void; } export interface ShowViewConfig { - user?: string; + user?: string; } export interface LovelaceBadgeConfig { - type?: string; - [key: string]: any; + type?: string; + [key: string]: any; } export interface LovelaceCardConfig { - index?: number; - view_index?: number; - view_layout?: any; - type: string; - [key: string]: any; + index?: number; + view_index?: number; + view_layout?: any; + type: string; + [key: string]: any; } export interface LovelaceLayoutOptions { - grid_columns?: number; - grid_rows?: number; + grid_columns?: number; + grid_rows?: number; } export interface ToggleActionConfig extends BaseActionConfig { - action: "toggle"; + action: "toggle"; } export interface CallServiceActionConfig extends BaseActionConfig { - action: "call-service"; - service: string; - target?: HassServiceTarget; - // "service_data" is kept for backwards compatibility. Replaced by "data". - service_data?: Record; - data?: Record; + action: "call-service"; + service: string; + target?: HassServiceTarget; + // "service_data" is kept for backwards compatibility. Replaced by "data". + service_data?: Record; + data?: Record; } export interface NavigateActionConfig extends BaseActionConfig { - action: "navigate"; - navigation_path: string; + action: "navigate"; + navigation_path: string; } export interface UrlActionConfig extends BaseActionConfig { - action: "url"; - url_path: string; + action: "url"; + url_path: string; } export interface MoreInfoActionConfig extends BaseActionConfig { - action: "more-info"; + action: "more-info"; } export interface NoActionConfig extends BaseActionConfig { - action: "none"; + action: "none"; } export interface CustomActionConfig extends BaseActionConfig { - action: "fire-dom-event"; + action: "fire-dom-event"; } export interface AssistActionConfig extends BaseActionConfig { - action: "assist"; - pipeline_id?: string; - start_listening?: boolean; + action: "assist"; + pipeline_id?: string; + start_listening?: boolean; } export interface BaseActionConfig { - action: string; - confirmation?: ConfirmationRestrictionConfig; + action: string; + confirmation?: ConfirmationRestrictionConfig; } export interface ConfirmationRestrictionConfig { - text?: string; - exemptions?: RestrictionConfig[]; + text?: string; + exemptions?: RestrictionConfig[]; } export interface RestrictionConfig { - user: string; + user: string; } export type ActionConfig = - | ToggleActionConfig - | CallServiceActionConfig - | NavigateActionConfig - | UrlActionConfig - | MoreInfoActionConfig - | AssistActionConfig - | NoActionConfig - | CustomActionConfig; + | ToggleActionConfig + | CallServiceActionConfig + | NavigateActionConfig + | UrlActionConfig + | MoreInfoActionConfig + | AssistActionConfig + | NoActionConfig + | CustomActionConfig; type LovelaceUpdatedEvent = HassEventBase & { - event_type: "lovelace_updated"; - data: { - url_path: string | null; - mode: "yaml" | "storage"; - }; + event_type: "lovelace_updated"; + data: { + url_path: string | null; + mode: "yaml" | "storage"; + }; }; export const fetchResources = (conn: Connection): Promise => - conn.sendMessagePromise({ - type: "lovelace/resources", - }); + conn.sendMessagePromise({ + type: "lovelace/resources", + }); -export const createResource = (hass: HomeAssistant, values: LovelaceResourcesMutableParams) => - hass.callWS({ - type: "lovelace/resources/create", - ...values, - }); +export const createResource = ( + hass: HomeAssistant, + values: LovelaceResourcesMutableParams +) => + hass.callWS({ + type: "lovelace/resources/create", + ...values, + }); export const updateResource = ( - hass: HomeAssistant, - id: string, - updates: Partial + hass: HomeAssistant, + id: string, + updates: Partial ) => - hass.callWS({ - type: "lovelace/resources/update", - resource_id: id, - ...updates, - }); + hass.callWS({ + type: "lovelace/resources/update", + resource_id: id, + ...updates, + }); export const deleteResource = (hass: HomeAssistant, id: string) => - hass.callWS({ - type: "lovelace/resources/delete", - resource_id: id, - }); - -export const fetchDashboards = (hass: HomeAssistant): Promise => - hass.callWS({ - type: "lovelace/dashboards/list", - }); - -export const createDashboard = (hass: HomeAssistant, values: LovelaceDashboardCreateParams) => - hass.callWS({ - type: "lovelace/dashboards/create", - ...values, - }); + hass.callWS({ + type: "lovelace/resources/delete", + resource_id: id, + }); + +export const fetchDashboards = ( + hass: HomeAssistant +): Promise => + hass.callWS({ + type: "lovelace/dashboards/list", + }); + +export const createDashboard = ( + hass: HomeAssistant, + values: LovelaceDashboardCreateParams +) => + hass.callWS({ + type: "lovelace/dashboards/create", + ...values, + }); export const updateDashboard = ( - hass: HomeAssistant, - id: string, - updates: Partial + hass: HomeAssistant, + id: string, + updates: Partial ) => - hass.callWS({ - type: "lovelace/dashboards/update", - dashboard_id: id, - ...updates, - }); + hass.callWS({ + type: "lovelace/dashboards/update", + dashboard_id: id, + ...updates, + }); export const deleteDashboard = (hass: HomeAssistant, id: string) => - hass.callWS({ - type: "lovelace/dashboards/delete", - dashboard_id: id, - }); + hass.callWS({ + type: "lovelace/dashboards/delete", + dashboard_id: id, + }); export const fetchConfig = ( - conn: Connection, - urlPath: string | null, - force: boolean + conn: Connection, + urlPath: string | null, + force: boolean ): Promise => - conn.sendMessagePromise({ - type: "lovelace/config", - url_path: urlPath, - force, - }); + conn.sendMessagePromise({ + type: "lovelace/config", + url_path: urlPath, + force, + }); export const saveConfig = ( - hass: HomeAssistant, - urlPath: string | null, - config: LovelaceConfig + hass: HomeAssistant, + urlPath: string | null, + config: LovelaceConfig +): Promise => + hass.callWS({ + type: "lovelace/config/save", + url_path: urlPath, + config, + }); + +export const deleteConfig = ( + hass: HomeAssistant, + urlPath: string | null ): Promise => - hass.callWS({ - type: "lovelace/config/save", - url_path: urlPath, - config, - }); - -export const deleteConfig = (hass: HomeAssistant, urlPath: string | null): Promise => - hass.callWS({ - type: "lovelace/config/delete", - url_path: urlPath, - }); + hass.callWS({ + type: "lovelace/config/delete", + url_path: urlPath, + }); export const subscribeLovelaceUpdates = ( - conn: Connection, - urlPath: string | null, - onChange: () => void + conn: Connection, + urlPath: string | null, + onChange: () => void ) => - conn.subscribeEvents((ev) => { - if (ev.data.url_path === urlPath) { - onChange(); - } - }, "lovelace_updated"); - -export const getLovelaceCollection = (conn: Connection, urlPath: string | null = null) => - getCollection( - conn, - `_lovelace_${urlPath ?? ""}`, - (conn2) => fetchConfig(conn2, urlPath, false), - (_conn, store) => - subscribeLovelaceUpdates(conn, urlPath, () => - fetchConfig(conn, urlPath, false).then((config) => store.setState(config, true)) - ) - ); + conn.subscribeEvents((ev) => { + if (ev.data.url_path === urlPath) { + onChange(); + } + }, "lovelace_updated"); + +export const getLovelaceCollection = ( + conn: Connection, + urlPath: string | null = null +) => + getCollection( + conn, + `_lovelace_${urlPath ?? ""}`, + (conn2) => fetchConfig(conn2, urlPath, false), + (_conn, store) => + subscribeLovelaceUpdates(conn, urlPath, () => + fetchConfig(conn, urlPath, false).then((config) => + store.setState(config, true) + ) + ) + ); // Legacy functions to support cast for Home Assistion < 0.107 -const fetchLegacyConfig = (conn: Connection, force: boolean): Promise => - conn.sendMessagePromise({ - type: "lovelace/config", - force, - }); +const fetchLegacyConfig = ( + conn: Connection, + force: boolean +): Promise => + conn.sendMessagePromise({ + type: "lovelace/config", + force, + }); -const subscribeLegacyLovelaceUpdates = (conn: Connection, onChange: () => void) => - conn.subscribeEvents(onChange, "lovelace_updated"); +const subscribeLegacyLovelaceUpdates = ( + conn: Connection, + onChange: () => void +) => conn.subscribeEvents(onChange, "lovelace_updated"); export const getLegacyLovelaceCollection = (conn: Connection) => - getCollection( - conn, - "_lovelace", - (conn2) => fetchLegacyConfig(conn2, false), - (_conn, store) => - subscribeLegacyLovelaceUpdates(conn, () => - fetchLegacyConfig(conn, false).then((config) => store.setState(config, true)) - ) - ); + getCollection( + conn, + "_lovelace", + (conn2) => fetchLegacyConfig(conn2, false), + (_conn, store) => + subscribeLegacyLovelaceUpdates(conn, () => + fetchLegacyConfig(conn, false).then((config) => + store.setState(config, true) + ) + ) + ); export interface WindowWithLovelaceProm extends Window { - llConfProm?: Promise; - llResProm?: Promise; + llConfProm?: Promise; + llResProm?: Promise; } export interface ActionHandlerOptions { - hasHold?: boolean; - hasDoubleClick?: boolean; - disabled?: boolean; + hasHold?: boolean; + hasDoubleClick?: boolean; + disabled?: boolean; } export interface ActionHandlerDetail { - action: "hold" | "tap" | "double_tap"; + action: "hold" | "tap" | "double_tap"; } export type ActionHandlerEvent = HASSDomEvent; diff --git a/src/ha/data/media-player.ts b/src/ha/data/media-player.ts index 986df623e..776a87d56 100644 --- a/src/ha/data/media-player.ts +++ b/src/ha/data/media-player.ts @@ -1,34 +1,44 @@ -import type { HassEntityAttributeBase, HassEntityBase } from "home-assistant-js-websocket"; +import type { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; import { HomeAssistant } from "../types"; interface MediaPlayerEntityAttributes extends HassEntityAttributeBase { - media_content_id?: string; - media_content_type?: string; - media_artist?: string; - media_playlist?: string; - media_series_title?: string; - media_season?: any; - media_episode?: any; - app_name?: string; - media_position_updated_at?: string | number | Date; - media_duration?: number; - media_position?: number; - media_title?: string; - icon?: string; - entity_picture_local?: string; - is_volume_muted?: boolean; - volume_level?: number; - repeat?: string; - shuffle?: boolean; - source?: string; - source_list?: string[]; - sound_mode?: string; - sound_mode_list?: string[]; + media_content_id?: string; + media_content_type?: string; + media_artist?: string; + media_playlist?: string; + media_series_title?: string; + media_season?: any; + media_episode?: any; + app_name?: string; + media_position_updated_at?: string | number | Date; + media_duration?: number; + media_position?: number; + media_title?: string; + icon?: string; + entity_picture_local?: string; + is_volume_muted?: boolean; + volume_level?: number; + repeat?: string; + shuffle?: boolean; + source?: string; + source_list?: string[]; + sound_mode?: string; + sound_mode_list?: string[]; } export interface MediaPlayerEntity extends HassEntityBase { - attributes: MediaPlayerEntityAttributes; - state: "playing" | "paused" | "idle" | "off" | "on" | "unavailable" | "unknown"; + attributes: MediaPlayerEntityAttributes; + state: + | "playing" + | "paused" + | "idle" + | "off" + | "on" + | "unavailable" + | "unknown"; } export const MEDIA_PLAYER_SUPPORT_PAUSE = 1; @@ -54,88 +64,92 @@ export type MediaPlayerBrowseAction = "pick" | "play"; export const BROWSER_PLAYER = "browser"; export type MediaClassBrowserSetting = { - icon: string; - thumbnail_ratio?: string; - layout?: "grid"; - show_list_images?: boolean; + icon: string; + thumbnail_ratio?: string; + layout?: "grid"; + show_list_images?: boolean; }; export interface MediaPlayerItemId { - media_content_id: string | undefined; - media_content_type: string | undefined; + media_content_id: string | undefined; + media_content_type: string | undefined; } export interface MediaPickedEvent { - item: MediaPlayerItem; - navigateIds: MediaPlayerItemId[]; + item: MediaPlayerItem; + navigateIds: MediaPlayerItemId[]; } export interface MediaPlayerThumbnail { - content_type: string; - content: string; + content_type: string; + content: string; } export interface MediaPlayerItem { - title: string; - media_content_type: string; - media_content_id: string; - media_class: string; - children_media_class?: string; - can_play: boolean; - can_expand: boolean; - thumbnail?: string; - children?: MediaPlayerItem[]; - not_shown?: number; + title: string; + media_content_type: string; + media_content_id: string; + media_class: string; + children_media_class?: string; + can_play: boolean; + can_expand: boolean; + thumbnail?: string; + children?: MediaPlayerItem[]; + not_shown?: number; } export const browseMediaPlayer = ( - hass: HomeAssistant, - entityId: string, - mediaContentId?: string, - mediaContentType?: string + hass: HomeAssistant, + entityId: string, + mediaContentId?: string, + mediaContentType?: string ): Promise => - hass.callWS({ - type: "media_player/browse_media", - entity_id: entityId, - media_content_id: mediaContentId, - media_content_type: mediaContentType, - }); + hass.callWS({ + type: "media_player/browse_media", + entity_id: entityId, + media_content_id: mediaContentId, + media_content_type: mediaContentType, + }); export const getCurrentProgress = (stateObj: MediaPlayerEntity): number => { - let progress = stateObj.attributes.media_position!; + let progress = stateObj.attributes.media_position!; - if (stateObj.state !== "playing") { - return progress; - } - progress += - (Date.now() - new Date(stateObj.attributes.media_position_updated_at!).getTime()) / 1000.0; + if (stateObj.state !== "playing") { return progress; + } + progress += + (Date.now() - + new Date(stateObj.attributes.media_position_updated_at!).getTime()) / + 1000.0; + return progress; }; -export const computeMediaDescription = (stateObj: MediaPlayerEntity): string => { - let secondaryTitle: string; - - switch (stateObj.attributes.media_content_type) { - case "music": - case "image": - secondaryTitle = stateObj.attributes.media_artist!; - break; - case "playlist": - secondaryTitle = stateObj.attributes.media_playlist!; - break; - case "tvshow": - secondaryTitle = stateObj.attributes.media_series_title!; - if (stateObj.attributes.media_season) { - secondaryTitle += " S" + stateObj.attributes.media_season; - - if (stateObj.attributes.media_episode) { - secondaryTitle += "E" + stateObj.attributes.media_episode; - } - } - break; - default: - secondaryTitle = stateObj.attributes.app_name || ""; - } - - return secondaryTitle; +export const computeMediaDescription = ( + stateObj: MediaPlayerEntity +): string => { + let secondaryTitle: string; + + switch (stateObj.attributes.media_content_type) { + case "music": + case "image": + secondaryTitle = stateObj.attributes.media_artist!; + break; + case "playlist": + secondaryTitle = stateObj.attributes.media_playlist!; + break; + case "tvshow": + secondaryTitle = stateObj.attributes.media_series_title!; + if (stateObj.attributes.media_season) { + secondaryTitle += " S" + stateObj.attributes.media_season; + + if (stateObj.attributes.media_episode) { + secondaryTitle += "E" + stateObj.attributes.media_episode; + } + } + break; + default: + secondaryTitle = stateObj.attributes.app_name || ""; + } + + return secondaryTitle; }; diff --git a/src/ha/data/translation.ts b/src/ha/data/translation.ts index 90971b739..4adf40f8b 100644 --- a/src/ha/data/translation.ts +++ b/src/ha/data/translation.ts @@ -1,70 +1,70 @@ export enum NumberFormat { - language = "language", - system = "system", - comma_decimal = "comma_decimal", - decimal_comma = "decimal_comma", - space_comma = "space_comma", - none = "none", + language = "language", + system = "system", + comma_decimal = "comma_decimal", + decimal_comma = "decimal_comma", + space_comma = "space_comma", + none = "none", } export enum TimeFormat { - language = "language", - system = "system", - am_pm = "12", - twenty_four = "24", + language = "language", + system = "system", + am_pm = "12", + twenty_four = "24", } export enum TimeZone { - local = "local", - server = "server", + local = "local", + server = "server", } export enum DateFormat { - language = "language", - system = "system", - DMY = "DMY", - MDY = "MDY", - YMD = "YMD", + language = "language", + system = "system", + DMY = "DMY", + MDY = "MDY", + YMD = "YMD", } export enum FirstWeekday { - language = "language", - monday = "monday", - tuesday = "tuesday", - wednesday = "wednesday", - thursday = "thursday", - friday = "friday", - saturday = "saturday", - sunday = "sunday", + language = "language", + monday = "monday", + tuesday = "tuesday", + wednesday = "wednesday", + thursday = "thursday", + friday = "friday", + saturday = "saturday", + sunday = "sunday", } export interface FrontendLocaleData { - language: string; - number_format: NumberFormat; - time_format: TimeFormat; - date_format: DateFormat; - first_weekday: FirstWeekday; - time_zone: TimeZone; + language: string; + number_format: NumberFormat; + time_format: TimeFormat; + date_format: DateFormat; + first_weekday: FirstWeekday; + time_zone: TimeZone; } declare global { - interface FrontendUserData { - language: FrontendLocaleData; - } + interface FrontendUserData { + language: FrontendLocaleData; + } } export type TranslationCategory = - | "title" - | "state" - | "entity" - | "entity_component" - | "config" - | "config_panel" - | "options" - | "device_automation" - | "mfa_setup" - | "system_health" - | "device_class" - | "application_credentials" - | "issues" - | "selector"; + | "title" + | "state" + | "entity" + | "entity_component" + | "config" + | "config_panel" + | "options" + | "device_automation" + | "mfa_setup" + | "system_health" + | "device_class" + | "application_credentials" + | "issues" + | "selector"; diff --git a/src/ha/data/update.ts b/src/ha/data/update.ts index 4b797eb00..e347dc57f 100644 --- a/src/ha/data/update.ts +++ b/src/ha/data/update.ts @@ -1,12 +1,15 @@ import type { - HassEntities, - HassEntity, - HassEntityAttributeBase, - HassEntityBase, + HassEntities, + HassEntity, + HassEntityAttributeBase, + HassEntityBase, } from "home-assistant-js-websocket"; import { BINARY_STATE_ON } from "../common/const"; import { computeStateDomain } from "../common/entity/compute_state_domain"; -import { supportsFeature, supportsFeatureFromAttributes } from "../common/entity/supports-feature"; +import { + supportsFeature, + supportsFeatureFromAttributes, +} from "../common/entity/supports-feature"; import { caseInsensitiveStringCompare } from "../common/string/compare"; import { HomeAssistant } from "../types"; @@ -17,74 +20,89 @@ export const UPDATE_SUPPORT_BACKUP = 8; export const UPDATE_SUPPORT_RELEASE_NOTES = 16; interface UpdateEntityAttributes extends HassEntityAttributeBase { - auto_update: boolean | null; - installed_version: string | null; - in_progress: boolean | number; - latest_version: string | null; - release_summary: string | null; - release_url: string | null; - skipped_version: string | null; - title: string | null; + auto_update: boolean | null; + installed_version: string | null; + in_progress: boolean | number; + latest_version: string | null; + release_summary: string | null; + release_url: string | null; + skipped_version: string | null; + title: string | null; } export interface UpdateEntity extends HassEntityBase { - attributes: UpdateEntityAttributes; + attributes: UpdateEntityAttributes; } export const updateUsesProgress = (entity: UpdateEntity): boolean => - updateUsesProgressFromAttributes(entity.attributes); + updateUsesProgressFromAttributes(entity.attributes); -export const updateUsesProgressFromAttributes = (attributes: { [key: string]: any }): boolean => - supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) && - typeof attributes.in_progress === "number"; +export const updateUsesProgressFromAttributes = (attributes: { + [key: string]: any; +}): boolean => + supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) && + typeof attributes.in_progress === "number"; -export const updateCanInstall = (entity: UpdateEntity, showSkipped = false): boolean => - (entity.state === BINARY_STATE_ON || - (showSkipped && Boolean(entity.attributes.skipped_version))) && - supportsFeature(entity, UPDATE_SUPPORT_INSTALL); +export const updateCanInstall = ( + entity: UpdateEntity, + showSkipped = false +): boolean => + (entity.state === BINARY_STATE_ON || + (showSkipped && Boolean(entity.attributes.skipped_version))) && + supportsFeature(entity, UPDATE_SUPPORT_INSTALL); export const updateIsInstalling = (entity: UpdateEntity): boolean => - updateUsesProgress(entity) || !!entity.attributes.in_progress; + updateUsesProgress(entity) || !!entity.attributes.in_progress; -export const updateIsInstallingFromAttributes = (attributes: { [key: string]: any }): boolean => - updateUsesProgressFromAttributes(attributes) || !!attributes.in_progress; +export const updateIsInstallingFromAttributes = (attributes: { + [key: string]: any; +}): boolean => + updateUsesProgressFromAttributes(attributes) || !!attributes.in_progress; export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) => - hass.callWS({ - type: "update/release_notes", - entity_id: entityId, - }); + hass.callWS({ + type: "update/release_notes", + entity_id: entityId, + }); -export const filterUpdateEntities = (entities: HassEntities, language?: string) => - ( - Object.values(entities).filter( - (stateObj) => computeStateDomain(stateObj) === "update" - ) as UpdateEntity[] - ).sort((a, b) => { - if (a.attributes.title === "Home Assistant Core") { - return -3; - } - if (b.attributes.title === "Home Assistant Core") { - return 3; - } - if (a.attributes.title === "Home Assistant Operating System") { - return -2; - } - if (b.attributes.title === "Home Assistant Operating System") { - return 2; - } - if (a.attributes.title === "Home Assistant Supervisor") { - return -1; - } - if (b.attributes.title === "Home Assistant Supervisor") { - return 1; - } - return caseInsensitiveStringCompare( - a.attributes.title || a.attributes.friendly_name || "", - b.attributes.title || b.attributes.friendly_name || "", - language - ); - }); +export const filterUpdateEntities = ( + entities: HassEntities, + language?: string +) => + ( + Object.values(entities).filter( + (stateObj) => computeStateDomain(stateObj) === "update" + ) as UpdateEntity[] + ).sort((a, b) => { + if (a.attributes.title === "Home Assistant Core") { + return -3; + } + if (b.attributes.title === "Home Assistant Core") { + return 3; + } + if (a.attributes.title === "Home Assistant Operating System") { + return -2; + } + if (b.attributes.title === "Home Assistant Operating System") { + return 2; + } + if (a.attributes.title === "Home Assistant Supervisor") { + return -1; + } + if (b.attributes.title === "Home Assistant Supervisor") { + return 1; + } + return caseInsensitiveStringCompare( + a.attributes.title || a.attributes.friendly_name || "", + b.attributes.title || b.attributes.friendly_name || "", + language + ); + }); -export const filterUpdateEntitiesWithInstall = (entities: HassEntities, showSkipped = false) => - filterUpdateEntities(entities).filter((entity) => updateCanInstall(entity, showSkipped)); +export const filterUpdateEntitiesWithInstall = ( + entities: HassEntities, + showSkipped = false +) => + filterUpdateEntities(entities).filter((entity) => + updateCanInstall(entity, showSkipped) + ); diff --git a/src/ha/data/vacuum.ts b/src/ha/data/vacuum.ts index a636b2102..603728a37 100644 --- a/src/ha/data/vacuum.ts +++ b/src/ha/data/vacuum.ts @@ -1,4 +1,7 @@ -import { HassEntityAttributeBase, HassEntityBase } from "home-assistant-js-websocket"; +import { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; export const STATE_ON = "on"; export const STATE_OFF = "off"; @@ -22,11 +25,11 @@ export const VACUUM_SUPPORT_STATE = 4096; export const VACUUM_SUPPORT_START = 8192; interface VacuumEntityAttributes extends HassEntityAttributeBase { - battery_level: number; - fan_speed: any; - [key: string]: any; + battery_level: number; + fan_speed: any; + [key: string]: any; } export interface VacuumEntity extends HassEntityBase { - attributes: VacuumEntityAttributes; + attributes: VacuumEntityAttributes; } diff --git a/src/ha/data/ws-templates.ts b/src/ha/data/ws-templates.ts index 2fd4145c6..57bfde655 100644 --- a/src/ha/data/ws-templates.ts +++ b/src/ha/data/ws-templates.ts @@ -1,29 +1,29 @@ import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket"; export interface RenderTemplateResult { - result: string; - listeners: TemplateListeners; + result: string; + listeners: TemplateListeners; } interface TemplateListeners { - all: boolean; - domains: string[]; - entities: string[]; - time: boolean; + all: boolean; + domains: string[]; + entities: string[]; + time: boolean; } export const subscribeRenderTemplate = ( - conn: Connection, - onChange: (result: RenderTemplateResult) => void, - params: { - template: string; - entity_ids?: string | string[]; - variables?: Record; - timeout?: number; - strict?: boolean; - } + conn: Connection, + onChange: (result: RenderTemplateResult) => void, + params: { + template: string; + entity_ids?: string | string[]; + variables?: Record; + timeout?: number; + strict?: boolean; + } ): Promise => - conn.subscribeMessage((msg: RenderTemplateResult) => onChange(msg), { - type: "render_template", - ...params, - }); + conn.subscribeMessage((msg: RenderTemplateResult) => onChange(msg), { + type: "render_template", + ...params, + }); diff --git a/src/ha/data/ws-themes.ts b/src/ha/data/ws-themes.ts index 900c8417e..b14581a9c 100644 --- a/src/ha/data/ws-themes.ts +++ b/src/ha/data/ws-themes.ts @@ -1,26 +1,26 @@ export interface ThemeVars { - // Incomplete - "primary-color": string; - "text-primary-color": string; - "accent-color": string; - [key: string]: string; + // Incomplete + "primary-color": string; + "text-primary-color": string; + "accent-color": string; + [key: string]: string; } export type Theme = ThemeVars & { - modes?: { - light?: ThemeVars; - dark?: ThemeVars; - }; + modes?: { + light?: ThemeVars; + dark?: ThemeVars; + }; }; export interface Themes { - default_theme: string; - default_dark_theme: string | null; - themes: Record; - // Currently effective dark mode. Will never be undefined. If user selected "auto" - // in theme picker, this property will still contain either true or false based on - // what has been determined via system preferences and support from the selected theme. - darkMode: boolean; - // Currently globally active theme name - theme: string; + default_theme: string; + default_dark_theme: string | null; + themes: Record; + // Currently effective dark mode. Will never be undefined. If user selected "auto" + // in theme picker, this property will still contain either true or false based on + // what has been determined via system preferences and support from the selected theme. + darkMode: boolean; + // Currently globally active theme name + theme: string; } diff --git a/src/ha/panels/lovelace/common/directives/action-handler-directive.ts b/src/ha/panels/lovelace/common/directives/action-handler-directive.ts index dcfc080dd..cb501309a 100644 --- a/src/ha/panels/lovelace/common/directives/action-handler-directive.ts +++ b/src/ha/panels/lovelace/common/directives/action-handler-directive.ts @@ -1,254 +1,271 @@ import type { Ripple } from "@material/mwc-ripple"; import { noChange } from "lit"; -import { AttributePart, directive, Directive, DirectiveParameters } from "lit/directive.js"; +import { + AttributePart, + directive, + Directive, + DirectiveParameters, +} from "lit/directive.js"; import { fireEvent } from "../../../../common/dom/fire_event"; import { deepEqual } from "../../../../common/util/deep-equal"; -import { ActionHandlerDetail, ActionHandlerOptions } from "../../../../data/lovelace"; +import { + ActionHandlerDetail, + ActionHandlerOptions, +} from "../../../../data/lovelace"; const isTouch = - "ontouchstart" in window || - navigator.maxTouchPoints > 0 || - // @ts-ignore - navigator.msMaxTouchPoints > 0; + "ontouchstart" in window || + navigator.maxTouchPoints > 0 || + // @ts-ignore + navigator.msMaxTouchPoints > 0; interface ActionHandler extends HTMLElement { - holdTime: number; - bind(element: Element, options?: ActionHandlerOptions): void; + holdTime: number; + bind(element: Element, options?: ActionHandlerOptions): void; } interface ActionHandlerElement extends HTMLElement { - actionHandler?: { - options: ActionHandlerOptions; - start?: (ev: Event) => void; - end?: (ev: Event) => void; - handleKeyDown?: (ev: KeyboardEvent) => void; - }; + actionHandler?: { + options: ActionHandlerOptions; + start?: (ev: Event) => void; + end?: (ev: Event) => void; + handleKeyDown?: (ev: KeyboardEvent) => void; + }; } declare global { - interface HTMLElementTagNameMap { - "action-handler": ActionHandler; - } - interface HASSDomEvents { - action: ActionHandlerDetail; - } + interface HTMLElementTagNameMap { + "action-handler": ActionHandler; + } + interface HASSDomEvents { + action: ActionHandlerDetail; + } } class ActionHandler extends HTMLElement implements ActionHandler { - public holdTime = 500; - - public ripple: Ripple; - - protected timer?: number; - - protected held = false; - - private cancelled = false; - - private dblClickTimeout?: number; - - constructor() { - super(); - this.ripple = document.createElement("mwc-ripple"); + public holdTime = 500; + + public ripple: Ripple; + + protected timer?: number; + + protected held = false; + + private cancelled = false; + + private dblClickTimeout?: number; + + constructor() { + super(); + this.ripple = document.createElement("mwc-ripple"); + } + + public connectedCallback() { + Object.assign(this.style, { + position: "fixed", + width: isTouch ? "100px" : "50px", + height: isTouch ? "100px" : "50px", + transform: "translate(-50%, -50%)", + pointerEvents: "none", + zIndex: "999", + }); + + this.appendChild(this.ripple); + this.ripple.primary = true; + + [ + "touchcancel", + "mouseout", + "mouseup", + "touchmove", + "mousewheel", + "wheel", + "scroll", + ].forEach((ev) => { + document.addEventListener( + ev, + () => { + this.cancelled = true; + if (this.timer) { + this.stopAnimation(); + clearTimeout(this.timer); + this.timer = undefined; + } + }, + { passive: true } + ); + }); + } + + public bind( + element: ActionHandlerElement, + options: ActionHandlerOptions = {} + ) { + if ( + element.actionHandler && + deepEqual(options, element.actionHandler.options) + ) { + return; } - public connectedCallback() { - Object.assign(this.style, { - position: "fixed", - width: isTouch ? "100px" : "50px", - height: isTouch ? "100px" : "50px", - transform: "translate(-50%, -50%)", - pointerEvents: "none", - zIndex: "999", - }); - - this.appendChild(this.ripple); - this.ripple.primary = true; - - [ - "touchcancel", - "mouseout", - "mouseup", - "touchmove", - "mousewheel", - "wheel", - "scroll", - ].forEach((ev) => { - document.addEventListener( - ev, - () => { - this.cancelled = true; - if (this.timer) { - this.stopAnimation(); - clearTimeout(this.timer); - this.timer = undefined; - } - }, - { passive: true } - ); - }); + if (element.actionHandler) { + element.removeEventListener("touchstart", element.actionHandler.start!); + element.removeEventListener("touchend", element.actionHandler.end!); + element.removeEventListener("touchcancel", element.actionHandler.end!); + + element.removeEventListener("mousedown", element.actionHandler.start!); + element.removeEventListener("click", element.actionHandler.end!); + + element.removeEventListener( + "keydown", + element.actionHandler.handleKeyDown! + ); + } else { + element.addEventListener("contextmenu", (ev: Event) => { + const e = ev || window.event; + if (e.preventDefault) { + e.preventDefault(); + } + if (e.stopPropagation) { + e.stopPropagation(); + } + e.cancelBubble = true; + e.returnValue = false; + return false; + }); } - public bind(element: ActionHandlerElement, options: ActionHandlerOptions = {}) { - if (element.actionHandler && deepEqual(options, element.actionHandler.options)) { - return; - } + element.actionHandler = { options }; - if (element.actionHandler) { - element.removeEventListener("touchstart", element.actionHandler.start!); - element.removeEventListener("touchend", element.actionHandler.end!); - element.removeEventListener("touchcancel", element.actionHandler.end!); + if (options.disabled) { + return; + } - element.removeEventListener("mousedown", element.actionHandler.start!); - element.removeEventListener("click", element.actionHandler.end!); + element.actionHandler.start = (ev: Event) => { + this.cancelled = false; + let x; + let y; + if ((ev as TouchEvent).touches) { + x = (ev as TouchEvent).touches[0].clientX; + y = (ev as TouchEvent).touches[0].clientY; + } else { + x = (ev as MouseEvent).clientX; + y = (ev as MouseEvent).clientY; + } + + if (options.hasHold) { + this.held = false; + this.timer = window.setTimeout(() => { + this.startAnimation(x, y); + this.held = true; + }, this.holdTime); + } + }; - element.removeEventListener("keydown", element.actionHandler.handleKeyDown!); + element.actionHandler.end = (ev: Event) => { + // Don't respond when moved or scrolled while touch + if (["touchend", "touchcancel"].includes(ev.type) && this.cancelled) { + return; + } + const target = ev.target as HTMLElement; + // Prevent mouse event if touch event + if (ev.cancelable) { + ev.preventDefault(); + } + if (options.hasHold) { + clearTimeout(this.timer); + this.stopAnimation(); + this.timer = undefined; + } + if (options.hasHold && this.held) { + fireEvent(target, "action", { action: "hold" }); + } else if (options.hasDoubleClick) { + if ( + (ev.type === "click" && (ev as MouseEvent).detail < 2) || + !this.dblClickTimeout + ) { + this.dblClickTimeout = window.setTimeout(() => { + this.dblClickTimeout = undefined; + fireEvent(target, "action", { action: "tap" }); + }, 250); } else { - element.addEventListener("contextmenu", (ev: Event) => { - const e = ev || window.event; - if (e.preventDefault) { - e.preventDefault(); - } - if (e.stopPropagation) { - e.stopPropagation(); - } - e.cancelBubble = true; - e.returnValue = false; - return false; - }); + clearTimeout(this.dblClickTimeout); + this.dblClickTimeout = undefined; + fireEvent(target, "action", { action: "double_tap" }); } + } else { + fireEvent(target, "action", { action: "tap" }); + } + }; - element.actionHandler = { options }; - - if (options.disabled) { - return; - } - - element.actionHandler.start = (ev: Event) => { - this.cancelled = false; - let x; - let y; - if ((ev as TouchEvent).touches) { - x = (ev as TouchEvent).touches[0].clientX; - y = (ev as TouchEvent).touches[0].clientY; - } else { - x = (ev as MouseEvent).clientX; - y = (ev as MouseEvent).clientY; - } - - if (options.hasHold) { - this.held = false; - this.timer = window.setTimeout(() => { - this.startAnimation(x, y); - this.held = true; - }, this.holdTime); - } - }; - - element.actionHandler.end = (ev: Event) => { - // Don't respond when moved or scrolled while touch - if (["touchend", "touchcancel"].includes(ev.type) && this.cancelled) { - return; - } - const target = ev.target as HTMLElement; - // Prevent mouse event if touch event - if (ev.cancelable) { - ev.preventDefault(); - } - if (options.hasHold) { - clearTimeout(this.timer); - this.stopAnimation(); - this.timer = undefined; - } - if (options.hasHold && this.held) { - fireEvent(target, "action", { action: "hold" }); - } else if (options.hasDoubleClick) { - if ( - (ev.type === "click" && (ev as MouseEvent).detail < 2) || - !this.dblClickTimeout - ) { - this.dblClickTimeout = window.setTimeout(() => { - this.dblClickTimeout = undefined; - fireEvent(target, "action", { action: "tap" }); - }, 250); - } else { - clearTimeout(this.dblClickTimeout); - this.dblClickTimeout = undefined; - fireEvent(target, "action", { action: "double_tap" }); - } - } else { - fireEvent(target, "action", { action: "tap" }); - } - }; - - element.actionHandler.handleKeyDown = (ev: KeyboardEvent) => { - if (!["Enter", " "].includes(ev.key)) { - return; - } - (ev.currentTarget as ActionHandlerElement).actionHandler!.end!(ev); - }; - - element.addEventListener("touchstart", element.actionHandler.start, { - passive: true, - }); - element.addEventListener("touchend", element.actionHandler.end); - element.addEventListener("touchcancel", element.actionHandler.end); - - element.addEventListener("mousedown", element.actionHandler.start, { - passive: true, - }); - element.addEventListener("click", element.actionHandler.end); - - element.addEventListener("keydown", element.actionHandler.handleKeyDown); - } - - private startAnimation(x: number, y: number) { - Object.assign(this.style, { - left: `${x}px`, - top: `${y}px`, - display: null, - }); - this.ripple.disabled = false; - this.ripple.startPress(); - this.ripple.unbounded = true; - } + element.actionHandler.handleKeyDown = (ev: KeyboardEvent) => { + if (!["Enter", " "].includes(ev.key)) { + return; + } + (ev.currentTarget as ActionHandlerElement).actionHandler!.end!(ev); + }; - private stopAnimation() { - this.ripple.endPress(); - this.ripple.disabled = true; - this.style.display = "none"; - } + element.addEventListener("touchstart", element.actionHandler.start, { + passive: true, + }); + element.addEventListener("touchend", element.actionHandler.end); + element.addEventListener("touchcancel", element.actionHandler.end); + + element.addEventListener("mousedown", element.actionHandler.start, { + passive: true, + }); + element.addEventListener("click", element.actionHandler.end); + + element.addEventListener("keydown", element.actionHandler.handleKeyDown); + } + + private startAnimation(x: number, y: number) { + Object.assign(this.style, { + left: `${x}px`, + top: `${y}px`, + display: null, + }); + this.ripple.disabled = false; + this.ripple.startPress(); + this.ripple.unbounded = true; + } + + private stopAnimation() { + this.ripple.endPress(); + this.ripple.disabled = true; + this.style.display = "none"; + } } const getActionHandler = (): ActionHandler => { - const body = document.body; - if (body.querySelector("action-handler")) { - return body.querySelector("action-handler") as ActionHandler; - } + const body = document.body; + if (body.querySelector("action-handler")) { + return body.querySelector("action-handler") as ActionHandler; + } - const actionhandler = document.createElement("action-handler"); - body.appendChild(actionhandler); + const actionhandler = document.createElement("action-handler"); + body.appendChild(actionhandler); - return actionhandler as ActionHandler; + return actionhandler as ActionHandler; }; export const actionHandlerBind = ( - element: ActionHandlerElement, - options?: ActionHandlerOptions + element: ActionHandlerElement, + options?: ActionHandlerOptions ) => { - const actionhandler: ActionHandler = getActionHandler(); - if (!actionhandler) { - return; - } - actionhandler.bind(element, options); + const actionhandler: ActionHandler = getActionHandler(); + if (!actionhandler) { + return; + } + actionhandler.bind(element, options); }; export const actionHandler = directive( - class extends Directive { - update(part: AttributePart, [options]: DirectiveParameters) { - actionHandlerBind(part.element as ActionHandlerElement, options); - return noChange; - } - - render(_options?: ActionHandlerOptions) {} + class extends Directive { + update(part: AttributePart, [options]: DirectiveParameters) { + actionHandlerBind(part.element as ActionHandlerElement, options); + return noChange; } + + render(_options?: ActionHandlerOptions) {} + } ); diff --git a/src/ha/panels/lovelace/common/entity/turn-on-off-entities.ts b/src/ha/panels/lovelace/common/entity/turn-on-off-entities.ts index 7e3b03fbb..82301043d 100644 --- a/src/ha/panels/lovelace/common/entity/turn-on-off-entities.ts +++ b/src/ha/panels/lovelace/common/entity/turn-on-off-entities.ts @@ -3,40 +3,40 @@ import { computeDomain } from "../../../../common/entity/compute_domain"; import { HomeAssistant } from "../../../../types"; export const turnOnOffEntities = ( - hass: HomeAssistant, - entityIds: string[], - turnOn = true + hass: HomeAssistant, + entityIds: string[], + turnOn = true ): void => { - const domainsToCall = {}; - entityIds.forEach((entityId) => { - const stateObj = hass.states[entityId]; - if (stateObj && STATES_OFF.includes(stateObj.state) === turnOn) { - const stateDomain = computeDomain(entityId); - const serviceDomain = ["cover", "lock"].includes(stateDomain) - ? stateDomain - : "homeassistant"; + const domainsToCall = {}; + entityIds.forEach((entityId) => { + const stateObj = hass.states[entityId]; + if (stateObj && STATES_OFF.includes(stateObj.state) === turnOn) { + const stateDomain = computeDomain(entityId); + const serviceDomain = ["cover", "lock"].includes(stateDomain) + ? stateDomain + : "homeassistant"; - if (!(serviceDomain in domainsToCall)) { - domainsToCall[serviceDomain] = []; - } - domainsToCall[serviceDomain].push(entityId); - } - }); + if (!(serviceDomain in domainsToCall)) { + domainsToCall[serviceDomain] = []; + } + domainsToCall[serviceDomain].push(entityId); + } + }); - Object.keys(domainsToCall).forEach((domain) => { - let service; - switch (domain) { - case "lock": - service = turnOn ? "unlock" : "lock"; - break; - case "cover": - service = turnOn ? "open_cover" : "close_cover"; - break; - default: - service = turnOn ? "turn_on" : "turn_off"; - } + Object.keys(domainsToCall).forEach((domain) => { + let service; + switch (domain) { + case "lock": + service = turnOn ? "unlock" : "lock"; + break; + case "cover": + service = turnOn ? "open_cover" : "close_cover"; + break; + default: + service = turnOn ? "turn_on" : "turn_off"; + } - const entities = domainsToCall[domain]; - hass.callService(domain, service, { entity_id: entities }); - }); + const entities = domainsToCall[domain]; + hass.callService(domain, service, { entity_id: entities }); + }); }; diff --git a/src/ha/panels/lovelace/common/entity/turn-on-off-entity.ts b/src/ha/panels/lovelace/common/entity/turn-on-off-entity.ts index 2ec5ac5bb..701c2c499 100644 --- a/src/ha/panels/lovelace/common/entity/turn-on-off-entity.ts +++ b/src/ha/panels/lovelace/common/entity/turn-on-off-entity.ts @@ -2,31 +2,31 @@ import { computeDomain } from "../../../../common/entity/compute_domain"; import { HomeAssistant, ServiceCallResponse } from "../../../../types"; export const turnOnOffEntity = ( - hass: HomeAssistant, - entityId: string, - turnOn = true + hass: HomeAssistant, + entityId: string, + turnOn = true ): Promise => { - const stateDomain = computeDomain(entityId); - const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain; + const stateDomain = computeDomain(entityId); + const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain; - let service; - switch (stateDomain) { - case "lock": - service = turnOn ? "unlock" : "lock"; - break; - case "cover": - service = turnOn ? "open_cover" : "close_cover"; - break; - case "button": - case "input_button": - service = "press"; - break; - case "scene": - service = "turn_on"; - break; - default: - service = turnOn ? "turn_on" : "turn_off"; - } + let service; + switch (stateDomain) { + case "lock": + service = turnOn ? "unlock" : "lock"; + break; + case "cover": + service = turnOn ? "open_cover" : "close_cover"; + break; + case "button": + case "input_button": + service = "press"; + break; + case "scene": + service = "turn_on"; + break; + default: + service = turnOn ? "turn_on" : "turn_off"; + } - return hass.callService(serviceDomain, service, { entity_id: entityId }); + return hass.callService(serviceDomain, service, { entity_id: entityId }); }; diff --git a/src/ha/panels/lovelace/common/handle-actions.ts b/src/ha/panels/lovelace/common/handle-actions.ts index ed50b6f82..157dd81f0 100644 --- a/src/ha/panels/lovelace/common/handle-actions.ts +++ b/src/ha/panels/lovelace/common/handle-actions.ts @@ -3,26 +3,26 @@ import { ActionConfig } from "../../../data/lovelace"; import { HomeAssistant } from "../../../types"; export type ActionConfigParams = { - entity?: string; - camera_image?: string; - hold_action?: ActionConfig; - tap_action?: ActionConfig; - double_tap_action?: ActionConfig; + entity?: string; + camera_image?: string; + hold_action?: ActionConfig; + tap_action?: ActionConfig; + double_tap_action?: ActionConfig; }; export const handleAction = async ( - node: HTMLElement, - _hass: HomeAssistant, - config: ActionConfigParams, - action: string + node: HTMLElement, + _hass: HomeAssistant, + config: ActionConfigParams, + action: string ): Promise => { - fireEvent(node, "hass-action", { config, action }); + fireEvent(node, "hass-action", { config, action }); }; type ActionParams = { config: ActionConfigParams; action: string }; declare global { - interface HASSDomEvents { - "hass-action": ActionParams; - } + interface HASSDomEvents { + "hass-action": ActionParams; + } } diff --git a/src/ha/panels/lovelace/common/has-action.ts b/src/ha/panels/lovelace/common/has-action.ts index 60c85a5de..caf973c77 100644 --- a/src/ha/panels/lovelace/common/has-action.ts +++ b/src/ha/panels/lovelace/common/has-action.ts @@ -1,5 +1,5 @@ import { ActionConfig } from "../../../data/lovelace"; export function hasAction(config?: ActionConfig): boolean { - return config !== undefined && config.action !== "none"; + return config !== undefined && config.action !== "none"; } diff --git a/src/ha/panels/lovelace/common/validate-condition.ts b/src/ha/panels/lovelace/common/validate-condition.ts index 76cc4f02c..2ae5df10a 100644 --- a/src/ha/panels/lovelace/common/validate-condition.ts +++ b/src/ha/panels/lovelace/common/validate-condition.ts @@ -2,19 +2,26 @@ import { UNAVAILABLE } from "../../../data/entity"; import { HomeAssistant } from "../../../types"; export interface Condition { - entity: string; - state?: string; - state_not?: string; + entity: string; + state?: string; + state_not?: string; } -export function checkConditionsMet(conditions: Condition[], hass: HomeAssistant): boolean { - return conditions.every((c) => { - const state = hass.states[c.entity] ? hass!.states[c.entity]?.state : UNAVAILABLE; +export function checkConditionsMet( + conditions: Condition[], + hass: HomeAssistant +): boolean { + return conditions.every((c) => { + const state = hass.states[c.entity] + ? hass!.states[c.entity]?.state + : UNAVAILABLE; - return c.state ? state === c.state : state !== c.state_not; - }); + return c.state ? state === c.state : state !== c.state_not; + }); } export function validateConditionalConfig(conditions: Condition[]): boolean { - return conditions.every((c) => (c.entity && (c.state || c.state_not)) as unknown as boolean); + return conditions.every( + (c) => (c.entity && (c.state || c.state_not)) as unknown as boolean + ); } diff --git a/src/ha/panels/lovelace/editor/structs/action-struct.ts b/src/ha/panels/lovelace/editor/structs/action-struct.ts index 1ece0540b..512e45aa8 100644 --- a/src/ha/panels/lovelace/editor/structs/action-struct.ts +++ b/src/ha/panels/lovelace/editor/structs/action-struct.ts @@ -1,91 +1,99 @@ import { - array, - boolean, - dynamic, - enums, - literal, - object, - optional, - string, - type, - union, + array, + boolean, + dynamic, + enums, + literal, + object, + optional, + string, + type, + union, } from "superstruct"; import { BaseActionConfig } from "../../../../data/lovelace"; const actionConfigStructUser = object({ - user: string(), + user: string(), }); const actionConfigStructConfirmation = union([ - boolean(), - object({ - text: optional(string()), - excemptions: optional(array(actionConfigStructUser)), - }), + boolean(), + object({ + text: optional(string()), + excemptions: optional(array(actionConfigStructUser)), + }), ]); const actionConfigStructUrl = object({ - action: literal("url"), - url_path: string(), - confirmation: optional(actionConfigStructConfirmation), + action: literal("url"), + url_path: string(), + confirmation: optional(actionConfigStructConfirmation), }); const actionConfigStructService = object({ - action: literal("call-service"), - service: string(), - service_data: optional(object()), - data: optional(object()), - target: optional( - object({ - entity_id: optional(union([string(), array(string())])), - device_id: optional(union([string(), array(string())])), - area_id: optional(union([string(), array(string())])), - }) - ), - confirmation: optional(actionConfigStructConfirmation), + action: literal("call-service"), + service: string(), + service_data: optional(object()), + data: optional(object()), + target: optional( + object({ + entity_id: optional(union([string(), array(string())])), + device_id: optional(union([string(), array(string())])), + area_id: optional(union([string(), array(string())])), + }) + ), + confirmation: optional(actionConfigStructConfirmation), }); const actionConfigStructNavigate = object({ - action: literal("navigate"), - navigation_path: string(), - confirmation: optional(actionConfigStructConfirmation), + action: literal("navigate"), + navigation_path: string(), + confirmation: optional(actionConfigStructConfirmation), }); const actionConfigStructAssist = type({ - action: literal("assist"), - pipeline_id: optional(string()), - start_listening: optional(boolean()), + action: literal("assist"), + pipeline_id: optional(string()), + start_listening: optional(boolean()), }); const actionConfigStructCustom = type({ - action: literal("fire-dom-event"), + action: literal("fire-dom-event"), }); export const actionConfigStructType = object({ - action: enums(["none", "toggle", "more-info", "call-service", "url", "navigate", "assist"]), - confirmation: optional(actionConfigStructConfirmation), + action: enums([ + "none", + "toggle", + "more-info", + "call-service", + "url", + "navigate", + "assist", + ]), + confirmation: optional(actionConfigStructConfirmation), }); export const actionConfigStruct = dynamic((value) => { - if (value && typeof value === "object" && "action" in value) { - switch ((value as BaseActionConfig).action!) { - case "call-service": { - return actionConfigStructService; - } - case "fire-dom-event": { - return actionConfigStructCustom; - } - case "navigate": { - return actionConfigStructNavigate; - } - case "url": { - return actionConfigStructUrl; - } - case "assist": { - return actionConfigStructAssist; - } - } + if (value && typeof value === "object" && "action" in value) { + switch ((value as BaseActionConfig).action!) { + case "call-service": { + return actionConfigStructService; + } + case "fire-dom-event": { + return actionConfigStructCustom; + } + case "navigate": { + return actionConfigStructNavigate; + } + case "url": { + return actionConfigStructUrl; + } + case "assist": { + return actionConfigStructAssist; + } } + } - return actionConfigStructType; + return actionConfigStructType; }); diff --git a/src/ha/panels/lovelace/types.ts b/src/ha/panels/lovelace/types.ts index 10bbcaece..9a1e2eda3 100644 --- a/src/ha/panels/lovelace/types.ts +++ b/src/ha/panels/lovelace/types.ts @@ -3,51 +3,51 @@ import { FrontendLocaleData } from "../../data/translation"; import { Constructor, HomeAssistant } from "../../types"; declare global { - // eslint-disable-next-line - interface HASSDomEvents { - "ll-rebuild": Record; - "ll-badge-rebuild": Record; - } + // eslint-disable-next-line + interface HASSDomEvents { + "ll-rebuild": Record; + "ll-badge-rebuild": Record; + } } export interface Lovelace { - config: LovelaceConfig; - // If not set, a strategy was used to generate everything - rawConfig: LovelaceConfig | undefined; - editMode: boolean; - urlPath: string | null; - mode: "generated" | "yaml" | "storage"; - locale: FrontendLocaleData; - enableFullEditMode: () => void; - setEditMode: (editMode: boolean) => void; - saveConfig: (newConfig: LovelaceConfig) => Promise; - deleteConfig: () => Promise; + config: LovelaceConfig; + // If not set, a strategy was used to generate everything + rawConfig: LovelaceConfig | undefined; + editMode: boolean; + urlPath: string | null; + mode: "generated" | "yaml" | "storage"; + locale: FrontendLocaleData; + enableFullEditMode: () => void; + setEditMode: (editMode: boolean) => void; + saveConfig: (newConfig: LovelaceConfig) => Promise; + deleteConfig: () => Promise; } export interface LovelaceCard extends HTMLElement { - hass?: HomeAssistant; - isPanel?: boolean; - editMode?: boolean; - getCardSize(): number | Promise; - setConfig(config: LovelaceCardConfig): void; + hass?: HomeAssistant; + isPanel?: boolean; + editMode?: boolean; + getCardSize(): number | Promise; + setConfig(config: LovelaceCardConfig): void; } export interface LovelaceCardConstructor extends Constructor { - getStubConfig?: ( - hass: HomeAssistant, - entities: string[], - entitiesFallback: string[] - ) => LovelaceCardConfig; - getConfigElement?: () => LovelaceCardEditor; + getStubConfig?: ( + hass: HomeAssistant, + entities: string[], + entitiesFallback: string[] + ) => LovelaceCardConfig; + getConfigElement?: () => LovelaceCardEditor; } export interface LovelaceCardEditor extends LovelaceGenericElementEditor { - setConfig(config: LovelaceCardConfig): void; + setConfig(config: LovelaceCardConfig): void; } export interface LovelaceGenericElementEditor extends HTMLElement { - hass?: HomeAssistant; - lovelace?: LovelaceConfig; - setConfig(config: any): void; - focusYamlEditor?: () => void; + hass?: HomeAssistant; + lovelace?: LovelaceConfig; + setConfig(config: any): void; + focusYamlEditor?: () => void; } diff --git a/src/ha/resources/ha-sortable-styles.ts b/src/ha/resources/ha-sortable-styles.ts index 9a7cd5603..8c7a5ded7 100644 --- a/src/ha/resources/ha-sortable-styles.ts +++ b/src/ha/resources/ha-sortable-styles.ts @@ -1,109 +1,109 @@ import { css } from "lit"; export const sortableStyles = css` - #sortable a:nth-of-type(2n) paper-icon-item { - animation-name: keyframes1; - animation-iteration-count: infinite; - transform-origin: 50% 10%; - animation-delay: -0.75s; - animation-duration: 0.25s; - } - - #sortable a:nth-of-type(2n-1) paper-icon-item { - animation-name: keyframes2; - animation-iteration-count: infinite; - animation-direction: alternate; - transform-origin: 30% 5%; - animation-delay: -0.5s; - animation-duration: 0.33s; - } - - #sortable a { - height: 48px; - display: flex; - } - - #sortable { - outline: none; - display: block !important; - } - - .hidden-panel { - display: flex !important; - } - - .sortable-fallback { - display: none; - } - - .sortable-ghost { - opacity: 0.4; - } - - .sortable-fallback { - opacity: 0; - } - - @keyframes keyframes1 { - 0% { - transform: rotate(-1deg); - animation-timing-function: ease-in; - } - - 50% { - transform: rotate(1.5deg); - animation-timing-function: ease-out; - } - } - - @keyframes keyframes2 { - 0% { - transform: rotate(1deg); - animation-timing-function: ease-in; - } - - 50% { - transform: rotate(-1.5deg); - animation-timing-function: ease-out; - } - } - - .show-panel, - .hide-panel { - display: none; - position: absolute; - top: 0; - right: 4px; - --mdc-icon-button-size: 40px; - } - - :host([rtl]) .show-panel { - right: initial; - left: 4px; - } - - .hide-panel { - top: 4px; - right: 8px; - } - - :host([rtl]) .hide-panel { - right: initial; - left: 8px; - } - - :host([expanded]) .hide-panel { - display: block; - } - - :host([expanded]) .show-panel { - display: inline-flex; - } - - paper-icon-item.hidden-panel, - paper-icon-item.hidden-panel span, - paper-icon-item.hidden-panel ha-icon[slot="item-icon"] { - color: var(--secondary-text-color); - cursor: pointer; - } + #sortable a:nth-of-type(2n) paper-icon-item { + animation-name: keyframes1; + animation-iteration-count: infinite; + transform-origin: 50% 10%; + animation-delay: -0.75s; + animation-duration: 0.25s; + } + + #sortable a:nth-of-type(2n-1) paper-icon-item { + animation-name: keyframes2; + animation-iteration-count: infinite; + animation-direction: alternate; + transform-origin: 30% 5%; + animation-delay: -0.5s; + animation-duration: 0.33s; + } + + #sortable a { + height: 48px; + display: flex; + } + + #sortable { + outline: none; + display: block !important; + } + + .hidden-panel { + display: flex !important; + } + + .sortable-fallback { + display: none; + } + + .sortable-ghost { + opacity: 0.4; + } + + .sortable-fallback { + opacity: 0; + } + + @keyframes keyframes1 { + 0% { + transform: rotate(-1deg); + animation-timing-function: ease-in; + } + + 50% { + transform: rotate(1.5deg); + animation-timing-function: ease-out; + } + } + + @keyframes keyframes2 { + 0% { + transform: rotate(1deg); + animation-timing-function: ease-in; + } + + 50% { + transform: rotate(-1.5deg); + animation-timing-function: ease-out; + } + } + + .show-panel, + .hide-panel { + display: none; + position: absolute; + top: 0; + right: 4px; + --mdc-icon-button-size: 40px; + } + + :host([rtl]) .show-panel { + right: initial; + left: 4px; + } + + .hide-panel { + top: 4px; + right: 8px; + } + + :host([rtl]) .hide-panel { + right: initial; + left: 8px; + } + + :host([expanded]) .hide-panel { + display: block; + } + + :host([expanded]) .show-panel { + display: inline-flex; + } + + paper-icon-item.hidden-panel, + paper-icon-item.hidden-panel span, + paper-icon-item.hidden-panel ha-icon[slot="item-icon"] { + color: var(--secondary-text-color); + cursor: pointer; + } `; diff --git a/src/ha/types.ts b/src/ha/types.ts index e811b1304..1b8282f24 100644 --- a/src/ha/types.ts +++ b/src/ha/types.ts @@ -1,225 +1,232 @@ import type { - Auth, - Connection, - HassConfig, - HassEntities, - HassEntity, - HassServices, - HassServiceTarget, - MessageBase, + Auth, + Connection, + HassConfig, + HassEntities, + HassEntity, + HassServices, + HassServiceTarget, + MessageBase, } from "home-assistant-js-websocket"; import type { LocalizeFunc } from "./common/translations/localize"; -import type { FrontendLocaleData, TranslationCategory } from "./data/translation"; +import type { + FrontendLocaleData, + TranslationCategory, +} from "./data/translation"; import type { Themes } from "./data/ws-themes"; declare global { - /* eslint-disable no-var, no-redeclare */ - var __DEV__: boolean; - var __DEMO__: boolean; - var __BUILD__: "latest" | "es5"; - var __VERSION__: string; - var __STATIC_PATH__: string; - var __BACKWARDS_COMPAT__: boolean; - var __SUPERVISOR__: boolean; - /* eslint-enable no-var, no-redeclare */ - - interface Window { - // Custom panel entry point url - customPanelJS: string; - ShadyCSS: { - nativeCss: boolean; - nativeShadow: boolean; - prepareTemplate(templateElement, elementName, elementExtension); - styleElement(element); - styleSubtree(element, overrideProperties); - styleDocument(overrideProperties); - getComputedStyleValue(element, propertyName); - }; - } - // for fire event - interface HASSDomEvents { - "value-changed": { - value: unknown; - }; - change: undefined; - } - - // For loading workers in webpack - interface ImportMeta { - url: string; - } + /* eslint-disable no-var, no-redeclare */ + var __DEV__: boolean; + var __DEMO__: boolean; + var __BUILD__: "latest" | "es5"; + var __VERSION__: string; + var __STATIC_PATH__: string; + var __BACKWARDS_COMPAT__: boolean; + var __SUPERVISOR__: boolean; + /* eslint-enable no-var, no-redeclare */ + + interface Window { + // Custom panel entry point url + customPanelJS: string; + ShadyCSS: { + nativeCss: boolean; + nativeShadow: boolean; + prepareTemplate(templateElement, elementName, elementExtension); + styleElement(element); + styleSubtree(element, overrideProperties); + styleDocument(overrideProperties); + getComputedStyleValue(element, propertyName); + }; + } + // for fire event + interface HASSDomEvents { + "value-changed": { + value: unknown; + }; + change: undefined; + } + + // For loading workers in webpack + interface ImportMeta { + url: string; + } } export interface EntityRegistryDisplayEntry { - entity_id: string; - name?: string; - device_id?: string; - area_id?: string; - hidden?: boolean; - entity_category?: "config" | "diagnostic"; - translation_key?: string; - platform?: string; - display_precision?: number; + entity_id: string; + name?: string; + device_id?: string; + area_id?: string; + hidden?: boolean; + entity_category?: "config" | "diagnostic"; + translation_key?: string; + platform?: string; + display_precision?: number; } export interface DeviceRegistryEntry { - id: string; - config_entries: string[]; - connections: Array<[string, string]>; - identifiers: Array<[string, string]>; - manufacturer: string | null; - model: string | null; - name: string | null; - sw_version: string | null; - hw_version: string | null; - via_device_id: string | null; - area_id: string | null; - name_by_user: string | null; - entry_type: "service" | null; - disabled_by: "user" | "integration" | "config_entry" | null; - configuration_url: string | null; + id: string; + config_entries: string[]; + connections: Array<[string, string]>; + identifiers: Array<[string, string]>; + manufacturer: string | null; + model: string | null; + name: string | null; + sw_version: string | null; + hw_version: string | null; + via_device_id: string | null; + area_id: string | null; + name_by_user: string | null; + entry_type: "service" | null; + disabled_by: "user" | "integration" | "config_entry" | null; + configuration_url: string | null; } export interface AreaRegistryEntry { - area_id: string; - name: string; - picture: string | null; + area_id: string; + name: string; + picture: string | null; } export interface ThemeSettings { - theme: string; - // Radio box selection for theme picker. Do not use in Lovelace rendering as - // it can be undefined == auto. - // Property hass.themes.darkMode carries effective current mode. - dark?: boolean; - primaryColor?: string; - accentColor?: string; + theme: string; + // Radio box selection for theme picker. Do not use in Lovelace rendering as + // it can be undefined == auto. + // Property hass.themes.darkMode carries effective current mode. + dark?: boolean; + primaryColor?: string; + accentColor?: string; } export interface PanelInfo | null> { - component_name: string; - config: T; - icon: string | null; - title: string | null; - url_path: string; + component_name: string; + config: T; + icon: string | null; + title: string | null; + url_path: string; } export interface Panels { - [name: string]: PanelInfo; + [name: string]: PanelInfo; } export interface Resources { - [language: string]: Record; + [language: string]: Record; } export interface Translation { - nativeName: string; - isRTL: boolean; - hash: string; + nativeName: string; + isRTL: boolean; + hash: string; } export interface TranslationMetadata { - fragments: string[]; - translations: { - [lang: string]: Translation; - }; + fragments: string[]; + translations: { + [lang: string]: Translation; + }; } export interface Credential { - auth_provider_type: string; - auth_provider_id: string; + auth_provider_type: string; + auth_provider_id: string; } export interface MFAModule { - id: string; - name: string; - enabled: boolean; + id: string; + name: string; + enabled: boolean; } export interface CurrentUser { - id: string; - is_owner: boolean; - is_admin: boolean; - name: string; - credentials: Credential[]; - mfa_modules: MFAModule[]; + id: string; + is_owner: boolean; + is_admin: boolean; + name: string; + credentials: Credential[]; + mfa_modules: MFAModule[]; } export interface ServiceCallRequest { - domain: string; - service: string; - serviceData?: Record; - target?: HassServiceTarget; + domain: string; + service: string; + serviceData?: Record; + target?: HassServiceTarget; } export interface Context { - id: string; - parent_id?: string; - user_id?: string | null; + id: string; + parent_id?: string; + user_id?: string | null; } export interface ServiceCallResponse { - context: Context; + context: Context; } export interface HomeAssistant { - auth: Auth; - connection: Connection; - connected: boolean; - states: HassEntities; - entities: { [id: string]: EntityRegistryDisplayEntry }; - devices: { [id: string]: DeviceRegistryEntry }; - areas: { [id: string]: AreaRegistryEntry }; - services: HassServices; - config: HassConfig; - themes: Themes; - selectedTheme: ThemeSettings | null; - panels: Panels; - panelUrl: string; - // i18n - // current effective language in that order: - // - backend saved user selected language - // - language in local app storage - // - browser language - // - english (en) - language: string; - // local stored language, keep that name for backward compatibility - selectedLanguage: string | null; - locale: FrontendLocaleData; - resources: Resources; - localize: LocalizeFunc; - translationMetadata: TranslationMetadata; - suspendWhenHidden: boolean; - enableShortcuts: boolean; - vibrate: boolean; - dockedSidebar: "docked" | "always_hidden" | "auto"; - defaultPanel: string; - moreInfoEntityId: string | null; - user?: CurrentUser; - hassUrl(path?): string; - callService( - domain: ServiceCallRequest["domain"], - service: ServiceCallRequest["service"], - serviceData?: ServiceCallRequest["serviceData"], - target?: ServiceCallRequest["target"] - ): Promise; - callApi( - method: "GET" | "POST" | "PUT" | "DELETE", - path: string, - parameters?: Record, - headers?: Record - ): Promise; - fetchWithAuth(path: string, init?: Record): Promise; - sendWS(msg: MessageBase): void; - callWS(msg: MessageBase): Promise; - loadBackendTranslation( - category: TranslationCategory, - integration?: string | string[], - configFlow?: boolean - ): Promise; - formatEntityState(stateObj: HassEntity, state?: string): string; - formatEntityAttributeValue(stateObj: HassEntity, attribute: string, value?: string): string; - formatEntityAttributeName(stateObj: HassEntity, attribute: string): string; + auth: Auth; + connection: Connection; + connected: boolean; + states: HassEntities; + entities: { [id: string]: EntityRegistryDisplayEntry }; + devices: { [id: string]: DeviceRegistryEntry }; + areas: { [id: string]: AreaRegistryEntry }; + services: HassServices; + config: HassConfig; + themes: Themes; + selectedTheme: ThemeSettings | null; + panels: Panels; + panelUrl: string; + // i18n + // current effective language in that order: + // - backend saved user selected language + // - language in local app storage + // - browser language + // - english (en) + language: string; + // local stored language, keep that name for backward compatibility + selectedLanguage: string | null; + locale: FrontendLocaleData; + resources: Resources; + localize: LocalizeFunc; + translationMetadata: TranslationMetadata; + suspendWhenHidden: boolean; + enableShortcuts: boolean; + vibrate: boolean; + dockedSidebar: "docked" | "always_hidden" | "auto"; + defaultPanel: string; + moreInfoEntityId: string | null; + user?: CurrentUser; + hassUrl(path?): string; + callService( + domain: ServiceCallRequest["domain"], + service: ServiceCallRequest["service"], + serviceData?: ServiceCallRequest["serviceData"], + target?: ServiceCallRequest["target"] + ): Promise; + callApi( + method: "GET" | "POST" | "PUT" | "DELETE", + path: string, + parameters?: Record, + headers?: Record + ): Promise; + fetchWithAuth(path: string, init?: Record): Promise; + sendWS(msg: MessageBase): void; + callWS(msg: MessageBase): Promise; + loadBackendTranslation( + category: TranslationCategory, + integration?: string | string[], + configFlow?: boolean + ): Promise; + formatEntityState(stateObj: HassEntity, state?: string): string; + formatEntityAttributeValue( + stateObj: HassEntity, + attribute: string, + value?: string + ): string; + formatEntityAttributeName(stateObj: HassEntity, attribute: string): string; } export type Constructor = new (...args: any[]) => T; diff --git a/src/ha/util.ts b/src/ha/util.ts index 52289bd30..a884ecd76 100644 --- a/src/ha/util.ts +++ b/src/ha/util.ts @@ -1,18 +1,20 @@ export const atLeastHaVersion = ( - version: string, - major: number, - minor: number, - patch?: number + version: string, + major: number, + minor: number, + patch?: number ): boolean => { - const [haMajor, haMinor, haPatch] = version.split(".", 3); + const [haMajor, haMinor, haPatch] = version.split(".", 3); - return ( - Number(haMajor) > major || - (Number(haMajor) === major && - (patch === undefined ? Number(haMinor) >= minor : Number(haMinor) > minor)) || - (patch !== undefined && - Number(haMajor) === major && - Number(haMinor) === minor && - Number(haPatch) >= patch) - ); + return ( + Number(haMajor) > major || + (Number(haMajor) === major && + (patch === undefined + ? Number(haMinor) >= minor + : Number(haMinor) > minor)) || + (patch !== undefined && + Number(haMajor) === major && + Number(haMinor) === minor && + Number(haPatch) >= patch) + ); }; diff --git a/src/localize.ts b/src/localize.ts index 80ca386a8..66018e30d 100644 --- a/src/localize.ts +++ b/src/localize.ts @@ -32,57 +32,60 @@ import * as zh_Hans from "./translations/zh-Hans.json"; import * as zh_Hant from "./translations/zh-Hant.json"; const languages: Record = { - ar, - bg, - ca, - cs, - da, - de, - el, - en, - es, - fi, - fr, - he, - hu, - id, - it, - "ko-KR": ko_KR, - nb, - nl, - pl, - "pt-BR": pt_BR, - "pt-PT": pt_PT, - ro, - ru, - sl, - sk, - sv, - tr, - uk, - vi, - "zh-Hans": zh_Hans, - "zh-Hant": zh_Hant, + ar, + bg, + ca, + cs, + da, + de, + el, + en, + es, + fi, + fr, + he, + hu, + id, + it, + "ko-KR": ko_KR, + nb, + nl, + pl, + "pt-BR": pt_BR, + "pt-PT": pt_PT, + ro, + ru, + sl, + sk, + sv, + tr, + uk, + vi, + "zh-Hans": zh_Hans, + "zh-Hant": zh_Hant, }; const DEFAULT_LANG = "en"; function getTranslatedString(key: string, lang: string): string | undefined { - try { - return key - .split(".") - .reduce((o, i) => (o as Record)[i], languages[lang]) as string; - } catch (_) { - return undefined; - } + try { + return key + .split(".") + .reduce( + (o, i) => (o as Record)[i], + languages[lang] + ) as string; + } catch (_) { + return undefined; + } } export default function setupCustomlocalize(hass?: HomeAssistant) { - return function (key: string) { - const lang = hass?.locale.language ?? DEFAULT_LANG; + return function (key: string) { + const lang = hass?.locale.language ?? DEFAULT_LANG; - let translated = getTranslatedString(key, lang); - if (!translated) translated = getTranslatedString(key, DEFAULT_LANG); - return translated ?? key; - }; + let translated = getTranslatedString(key, lang); + if (!translated) translated = getTranslatedString(key, DEFAULT_LANG); + return translated ?? key; + }; } diff --git a/src/mushroom.ts b/src/mushroom.ts index 0ddd7624b..1c4be62b4 100644 --- a/src/mushroom.ts +++ b/src/mushroom.ts @@ -23,4 +23,7 @@ import "./cards/title-card/title-card"; import "./cards/update-card/update-card"; import "./cards/vacuum-card/vacuum-card"; -console.info(`%c🍄 Mushroom 🍄 - ${version}`, "color: #ef5350; font-weight: 700;"); +console.info( + `%c🍄 Mushroom 🍄 - ${version}`, + "color: #ef5350; font-weight: 700;" +); diff --git a/src/shared/badge-icon.ts b/src/shared/badge-icon.ts index 13d1cb1b0..5e427e13c 100644 --- a/src/shared/badge-icon.ts +++ b/src/shared/badge-icon.ts @@ -3,38 +3,38 @@ import { property, customElement } from "lit/decorators.js"; @customElement("mushroom-badge-icon") export class BadgeIcon extends LitElement { - @property() public icon: string = ""; + @property() public icon: string = ""; - protected render(): TemplateResult { - return html` -
- -
- `; - } + protected render(): TemplateResult { + return html` +
+ +
+ `; + } - static get styles(): CSSResultGroup { - return css` - :host { - --main-color: rgb(var(--rgb-grey)); - --icon-color: rgb(var(--rgb-white)); - } - .badge { - display: flex; - align-items: center; - justify-content: center; - line-height: 0; - width: var(--badge-size); - height: var(--badge-size); - font-size: var(--badge-size); - border-radius: var(--badge-border-radius); - background-color: var(--main-color); - transition: background-color 280ms ease-in-out; - } - .badge ha-icon { - --mdc-icon-size: var(--badge-icon-size); - color: var(--icon-color); - } - `; - } + static get styles(): CSSResultGroup { + return css` + :host { + --main-color: rgb(var(--rgb-grey)); + --icon-color: rgb(var(--rgb-white)); + } + .badge { + display: flex; + align-items: center; + justify-content: center; + line-height: 0; + width: var(--badge-size); + height: var(--badge-size); + font-size: var(--badge-size); + border-radius: var(--badge-border-radius); + background-color: var(--main-color); + transition: background-color 280ms ease-in-out; + } + .badge ha-icon { + --mdc-icon-size: var(--badge-icon-size); + color: var(--icon-color); + } + `; + } } diff --git a/src/shared/button-group.ts b/src/shared/button-group.ts index 1d62a27c1..88f7c6643 100644 --- a/src/shared/button-group.ts +++ b/src/shared/button-group.ts @@ -4,59 +4,61 @@ import { classMap } from "lit/directives/class-map.js"; @customElement("mushroom-button-group") export class MushroomButtonGroup extends LitElement { - @property() public fill: boolean = false; + @property() public fill: boolean = false; - @property() public rtl: boolean = false; + @property() public rtl: boolean = false; - protected render(): TemplateResult { - return html` -
- -
- `; - } + protected render(): TemplateResult { + return html` +
+ +
+ `; + } - static get styles(): CSSResultGroup { - return css` - :host { - display: flex; - flex-direction: row; - width: 100%; - } - .container { - width: 100%; - display: flex; - flex-direction: row; - justify-content: flex-end; - } - .container ::slotted(*:not(:last-child)) { - margin-right: var(--spacing); - } - :host([rtl]) .container ::slotted(*:not(:last-child)) { - margin-right: initial; - margin-left: var(--spacing); - } - .container > ::slotted(mushroom-button) { - width: 0; - flex-grow: 0; - flex-shrink: 1; - flex-basis: calc(var(--control-height) * var(--control-button-ratio)); - } - .container > ::slotted(mushroom-input-number) { - width: 0; - flex-grow: 0; - flex-shrink: 1; - flex-basis: calc(var(--control-height) * var(--control-button-ratio) * 3); - } - .container.fill > ::slotted(mushroom-button), - .container.fill > ::slotted(mushroom-input-number) { - flex-grow: 1; - } - `; - } + static get styles(): CSSResultGroup { + return css` + :host { + display: flex; + flex-direction: row; + width: 100%; + } + .container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: flex-end; + } + .container ::slotted(*:not(:last-child)) { + margin-right: var(--spacing); + } + :host([rtl]) .container ::slotted(*:not(:last-child)) { + margin-right: initial; + margin-left: var(--spacing); + } + .container > ::slotted(mushroom-button) { + width: 0; + flex-grow: 0; + flex-shrink: 1; + flex-basis: calc(var(--control-height) * var(--control-button-ratio)); + } + .container > ::slotted(mushroom-input-number) { + width: 0; + flex-grow: 0; + flex-shrink: 1; + flex-basis: calc( + var(--control-height) * var(--control-button-ratio) * 3 + ); + } + .container.fill > ::slotted(mushroom-button), + .container.fill > ::slotted(mushroom-input-number) { + flex-grow: 1; + } + `; + } } diff --git a/src/shared/button.ts b/src/shared/button.ts index 20cbbff53..1268b06c7 100644 --- a/src/shared/button.ts +++ b/src/shared/button.ts @@ -3,57 +3,62 @@ import { property, customElement } from "lit/decorators.js"; @customElement("mushroom-button") export class Button extends LitElement { - @property() public title: string = ""; - @property({ type: Boolean }) public disabled: boolean = false; + @property() public title: string = ""; + @property({ type: Boolean }) public disabled: boolean = false; - protected render(): TemplateResult { - return html` - - `; - } + protected render(): TemplateResult { + return html` + + `; + } - static get styles(): CSSResultGroup { - return css` - :host { - --icon-color: var(--primary-text-color); - --icon-color-disabled: rgb(var(--rgb-disabled)); - --bg-color: rgba(var(--rgb-primary-text-color), 0.05); - --bg-color-disabled: rgba(var(--rgb-disabled), 0.2); - height: var(--control-height); - width: calc(var(--control-height) * var(--control-button-ratio)); - flex: none; - } - .button { - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - border-radius: var(--control-border-radius); - border: none; - background-color: var(--bg-color); - transition: background-color 280ms ease-in-out; - font-size: var(--control-height); - margin: 0; - padding: 0; - box-sizing: border-box; - line-height: 0; - } - .button:disabled { - cursor: not-allowed; - background-color: var(--bg-color-disabled); - } - .button ::slotted(*) { - --mdc-icon-size: var(--control-icon-size); - color: var(--icon-color); - pointer-events: none; - } - .button:disabled ::slotted(*) { - color: var(--icon-color-disabled); - } - `; - } + static get styles(): CSSResultGroup { + return css` + :host { + --icon-color: var(--primary-text-color); + --icon-color-disabled: rgb(var(--rgb-disabled)); + --bg-color: rgba(var(--rgb-primary-text-color), 0.05); + --bg-color-disabled: rgba(var(--rgb-disabled), 0.2); + height: var(--control-height); + width: calc(var(--control-height) * var(--control-button-ratio)); + flex: none; + } + .button { + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + border-radius: var(--control-border-radius); + border: none; + background-color: var(--bg-color); + transition: background-color 280ms ease-in-out; + font-size: var(--control-height); + margin: 0; + padding: 0; + box-sizing: border-box; + line-height: 0; + } + .button:disabled { + cursor: not-allowed; + background-color: var(--bg-color-disabled); + } + .button ::slotted(*) { + --mdc-icon-size: var(--control-icon-size); + color: var(--icon-color); + pointer-events: none; + } + .button:disabled ::slotted(*) { + color: var(--icon-color-disabled); + } + `; + } } diff --git a/src/shared/card.ts b/src/shared/card.ts index cca62383b..ced39ac33 100644 --- a/src/shared/card.ts +++ b/src/shared/card.ts @@ -5,63 +5,63 @@ import { Appearance } from "./config/appearance-config"; @customElement("mushroom-card") export class Card extends LitElement { - @property() public appearance?: Appearance; + @property() public appearance?: Appearance; - protected render(): TemplateResult { - return html` -
- -
- `; - } + protected render(): TemplateResult { + return html` +
+ +
+ `; + } - static get styles(): CSSResultGroup { - return css` - .container { - display: flex; - flex-direction: column; - flex-shrink: 0; - flex-grow: 0; - box-sizing: border-box; - justify-content: space-between; - height: 100%; - } - .container > ::slotted(*:not(:last-child)) { - margin-bottom: var(--spacing); - } - .container.horizontal { - flex-direction: row; - } - .container.horizontal > ::slotted(*) { - flex: 1; - min-width: 0; - } - .container.no-info > ::slotted(mushroom-state-item) { - flex: none; - } - .container.no-info.no-icon > ::slotted(mushroom-state-item) { - margin-right: 0; - margin-left: 0; - margin-bottom: 0; - } - .container.horizontal > ::slotted(*:not(:last-child)) { - margin-right: var(--spacing); - margin-bottom: 0; - } - :host([rtl]) .container.horizontal > ::slotted(*:not(:last-child)) { - margin-right: initial; - margin-left: var(--spacing); - margin-bottom: 0; - } - `; - } + static get styles(): CSSResultGroup { + return css` + .container { + display: flex; + flex-direction: column; + flex-shrink: 0; + flex-grow: 0; + box-sizing: border-box; + justify-content: space-between; + height: 100%; + } + .container > ::slotted(*:not(:last-child)) { + margin-bottom: var(--spacing); + } + .container.horizontal { + flex-direction: row; + } + .container.horizontal > ::slotted(*) { + flex: 1; + min-width: 0; + } + .container.no-info > ::slotted(mushroom-state-item) { + flex: none; + } + .container.no-info.no-icon > ::slotted(mushroom-state-item) { + margin-right: 0; + margin-left: 0; + margin-bottom: 0; + } + .container.horizontal > ::slotted(*:not(:last-child)) { + margin-right: var(--spacing); + margin-bottom: 0; + } + :host([rtl]) .container.horizontal > ::slotted(*:not(:last-child)) { + margin-right: initial; + margin-left: var(--spacing); + margin-bottom: 0; + } + `; + } } diff --git a/src/shared/chip.ts b/src/shared/chip.ts index 4c0dde32e..4d1f67016 100644 --- a/src/shared/chip.ts +++ b/src/shared/chip.ts @@ -1,105 +1,116 @@ -import { css, CSSResultGroup, html, LitElement, nothing, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + nothing, + TemplateResult, +} from "lit"; import { property, customElement } from "lit/decorators.js"; import { animations } from "../utils/entity-styles"; @customElement("mushroom-chip") export class Chip extends LitElement { - @property() public icon: string = ""; + @property() public icon: string = ""; - @property() public label: string = ""; + @property() public label: string = ""; - @property() public avatar: string = ""; + @property() public avatar: string = ""; - @property() public avatarOnly: boolean = false; + @property() public avatarOnly: boolean = false; - protected render(): TemplateResult { - return html` - - ${this.avatar ? html` ` : nothing} - ${!this.avatarOnly - ? html` -
- -
- ` - : nothing} -
- `; - } + protected render(): TemplateResult { + return html` + + ${this.avatar + ? html` ` + : nothing} + ${!this.avatarOnly + ? html` +
+ +
+ ` + : nothing} +
+ `; + } - static get styles(): CSSResultGroup { - return [ - // animations must be on this element for safari - animations, - css` - :host { - --icon-color: var(--primary-text-color); - --text-color: var(--primary-text-color); - } - ha-card { - box-sizing: border-box; - height: var(--chip-height); - min-width: var(--chip-height); - font-size: var(--chip-height); - width: auto; - border-radius: var(--chip-border-radius); - display: flex; - flex-direction: row; - align-items: center; - background: var(--chip-background); - border-width: var(--chip-border-width); - border-color: var(--chip-border-color); - box-shadow: var(--chip-box-shadow); - box-sizing: content-box; - } - .avatar { - --avatar-size: calc(var(--chip-height) - 2 * var(--chip-avatar-padding)); - border-radius: var(--chip-avatar-border-radius); - height: var(--avatar-size); - width: var(--avatar-size); - margin-left: var(--chip-avatar-padding); - box-sizing: border-box; - object-fit: cover; - } - :host([rtl]) .avatar { - margin-left: initial; - margin-right: var(--chip-avatar-padding); - } - .content { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - height: 100%; - padding: var(--chip-padding); - line-height: 0; - } - ::slotted(ha-icon), - ::slotted(ha-state-icon) { - display: flex; - line-height: 0; - --mdc-icon-size: var(--chip-icon-size); - color: var(--icon-color); - } - ::slotted(svg) { - width: var(--chip-icon-size); - height: var(--chip-icon-size); - display: flex; - } - ::slotted(span) { - font-weight: var(--chip-font-weight); - font-size: var(--chip-font-size); - line-height: 1; - color: var(--text-color); - } - ::slotted(*:not(:last-child)) { - margin-right: 0.15em; - } - :host([rtl]) ::slotted(*:not(:last-child)) { - margin-right: initial; - margin-left: 0.15em; - } - `, - ]; - } + static get styles(): CSSResultGroup { + return [ + // animations must be on this element for safari + animations, + css` + :host { + --icon-color: var(--primary-text-color); + --text-color: var(--primary-text-color); + } + ha-card { + box-sizing: border-box; + height: var(--chip-height); + min-width: var(--chip-height); + font-size: var(--chip-height); + width: auto; + border-radius: var(--chip-border-radius); + display: flex; + flex-direction: row; + align-items: center; + background: var(--chip-background); + border-width: var(--chip-border-width); + border-color: var(--chip-border-color); + box-shadow: var(--chip-box-shadow); + box-sizing: content-box; + } + .avatar { + --avatar-size: calc( + var(--chip-height) - 2 * var(--chip-avatar-padding) + ); + border-radius: var(--chip-avatar-border-radius); + height: var(--avatar-size); + width: var(--avatar-size); + margin-left: var(--chip-avatar-padding); + box-sizing: border-box; + object-fit: cover; + } + :host([rtl]) .avatar { + margin-left: initial; + margin-right: var(--chip-avatar-padding); + } + .content { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + height: 100%; + padding: var(--chip-padding); + line-height: 0; + } + ::slotted(ha-icon), + ::slotted(ha-state-icon) { + display: flex; + line-height: 0; + --mdc-icon-size: var(--chip-icon-size); + color: var(--icon-color); + } + ::slotted(svg) { + width: var(--chip-icon-size); + height: var(--chip-icon-size); + display: flex; + } + ::slotted(span) { + font-weight: var(--chip-font-weight); + font-size: var(--chip-font-size); + line-height: 1; + color: var(--text-color); + } + ::slotted(*:not(:last-child)) { + margin-right: 0.15em; + } + :host([rtl]) ::slotted(*:not(:last-child)) { + margin-right: initial; + margin-left: 0.15em; + } + `, + ]; + } } diff --git a/src/shared/config/actions-config.ts b/src/shared/config/actions-config.ts index 7d6ff3e46..d2323ed80 100644 --- a/src/shared/config/actions-config.ts +++ b/src/shared/config/actions-config.ts @@ -4,30 +4,32 @@ import { HaFormSchema } from "../../utils/form/ha-form"; import { UiAction } from "../../utils/form/ha-selector"; export const actionsSharedConfigStruct = object({ - tap_action: optional(actionConfigStruct), - hold_action: optional(actionConfigStruct), - double_tap_action: optional(actionConfigStruct), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + double_tap_action: optional(actionConfigStruct), }); export type ActionsSharedConfig = { - tap_action?: ActionConfig; - hold_action?: ActionConfig; - double_tap_action?: ActionConfig; + tap_action?: ActionConfig; + hold_action?: ActionConfig; + double_tap_action?: ActionConfig; }; -export const computeActionsFormSchema = (actions?: UiAction[]): HaFormSchema[] => { - return [ - { - name: "tap_action", - selector: { "ui-action": { actions } }, - }, - { - name: "hold_action", - selector: { "ui-action": { actions } }, - }, - { - name: "double_tap_action", - selector: { "ui-action": { actions } }, - }, - ]; +export const computeActionsFormSchema = ( + actions?: UiAction[] +): HaFormSchema[] => { + return [ + { + name: "tap_action", + selector: { "ui-action": { actions } }, + }, + { + name: "hold_action", + selector: { "ui-action": { actions } }, + }, + { + name: "double_tap_action", + selector: { "ui-action": { actions } }, + }, + ]; }; diff --git a/src/shared/config/appearance-config.ts b/src/shared/config/appearance-config.ts index 3575491b4..e4205f724 100644 --- a/src/shared/config/appearance-config.ts +++ b/src/shared/config/appearance-config.ts @@ -4,39 +4,39 @@ import { IconType, ICON_TYPES, Info, INFOS } from "../../utils/info"; import { Layout, layoutStruct } from "../../utils/layout"; export const appearanceSharedConfigStruct = object({ - layout: optional(layoutStruct), - fill_container: optional(boolean()), - primary_info: optional(enums(INFOS)), - secondary_info: optional(enums(INFOS)), - icon_type: optional(enums(ICON_TYPES)), + layout: optional(layoutStruct), + fill_container: optional(boolean()), + primary_info: optional(enums(INFOS)), + secondary_info: optional(enums(INFOS)), + icon_type: optional(enums(ICON_TYPES)), }); export type AppearanceSharedConfig = Infer; export type Appearance = { - layout: Layout; - fill_container: boolean; - primary_info: Info; - secondary_info: Info; - icon_type: IconType; + layout: Layout; + fill_container: boolean; + primary_info: Info; + secondary_info: Info; + icon_type: IconType; }; export const APPEARANCE_FORM_SCHEMA: HaFormSchema[] = [ - { - type: "grid", - name: "", - schema: [ - { name: "layout", selector: { mush_layout: {} } }, - { name: "fill_container", selector: { boolean: {} } }, - ], - }, - { - type: "grid", - name: "", - schema: [ - { name: "primary_info", selector: { mush_info: {} } }, - { name: "secondary_info", selector: { mush_info: {} } }, - { name: "icon_type", selector: { mush_icon_type: {} } }, - ], - }, + { + type: "grid", + name: "", + schema: [ + { name: "layout", selector: { mush_layout: {} } }, + { name: "fill_container", selector: { boolean: {} } }, + ], + }, + { + type: "grid", + name: "", + schema: [ + { name: "primary_info", selector: { mush_info: {} } }, + { name: "secondary_info", selector: { mush_info: {} } }, + { name: "icon_type", selector: { mush_icon_type: {} } }, + ], + }, ]; diff --git a/src/shared/config/entity-config.ts b/src/shared/config/entity-config.ts index fe8176b72..86c278cf0 100644 --- a/src/shared/config/entity-config.ts +++ b/src/shared/config/entity-config.ts @@ -1,9 +1,9 @@ import { Infer, object, optional, string } from "superstruct"; export const entitySharedConfigStruct = object({ - entity: optional(string()), - name: optional(string()), - icon: optional(string()), + entity: optional(string()), + name: optional(string()), + icon: optional(string()), }); export type EntitySharedConfig = Infer; diff --git a/src/shared/config/lovelace-card-config.ts b/src/shared/config/lovelace-card-config.ts index 81ed69cbd..4c421062b 100644 --- a/src/shared/config/lovelace-card-config.ts +++ b/src/shared/config/lovelace-card-config.ts @@ -1,10 +1,10 @@ import { any, number, object, optional, string } from "superstruct"; export const lovelaceCardConfigStruct = object({ - index: optional(number()), - view_index: optional(number()), - view_layout: any(), - type: string(), - layout_options: any(), - visibility: any(), + index: optional(number()), + view_index: optional(number()), + view_layout: any(), + type: string(), + layout_options: any(), + visibility: any(), }); diff --git a/src/shared/editor/alignment-picker.ts b/src/shared/editor/alignment-picker.ts index 14cf69a9b..0b3e6d3a9 100644 --- a/src/shared/editor/alignment-picker.ts +++ b/src/shared/editor/alignment-picker.ts @@ -8,70 +8,72 @@ const ALIGNMENT = ["default", "start", "center", "end", "justify"] as const; type Alignment = (typeof ALIGNMENT)[number]; const ICONS: Record = { - default: "mdi:format-align-left", - start: "mdi:format-align-left", - center: "mdi:format-align-center", - end: "mdi:format-align-right", - justify: "mdi:format-align-justify", + default: "mdi:format-align-left", + start: "mdi:format-align-left", + center: "mdi:format-align-center", + end: "mdi:format-align-right", + justify: "mdi:format-align-justify", }; @customElement("mushroom-alignment-picker") export class AlignmentPicker extends LitElement { - @property() public label = ""; + @property() public label = ""; - @property() public value?: string; + @property() public value?: string; - @property() public configValue = ""; + @property() public configValue = ""; - @property() public hass!: HomeAssistant; + @property() public hass!: HomeAssistant; - _selectChanged(ev) { - const value = ev.target.value; - if (value) { - this.dispatchEvent( - new CustomEvent("value-changed", { - detail: { - value: value !== "default" ? value : "", - }, - }) - ); - } + _selectChanged(ev) { + const value = ev.target.value; + if (value) { + this.dispatchEvent( + new CustomEvent("value-changed", { + detail: { + value: value !== "default" ? value : "", + }, + }) + ); } + } - render() { - const customLocalize = setupCustomlocalize(this.hass); + render() { + const customLocalize = setupCustomlocalize(this.hass); - const value = this.value || "default"; + const value = this.value || "default"; - return html` - e.stopPropagation()} - .value=${this.value || "default"} - fixedMenuPosition - naturalMenuWidth - > - - ${ALIGNMENT.map((alignment) => { - return html` - - ${customLocalize(`editor.form.alignment_picker.values.${alignment}`)} - - - `; - })} - - `; - } + return html` + e.stopPropagation()} + .value=${this.value || "default"} + fixedMenuPosition + naturalMenuWidth + > + + ${ALIGNMENT.map((alignment) => { + return html` + + ${customLocalize( + `editor.form.alignment_picker.values.${alignment}` + )} + + + `; + })} + + `; + } - static get styles(): CSSResultGroup { - return css` - mushroom-select { - width: 100%; - } - `; - } + static get styles(): CSSResultGroup { + return css` + mushroom-select { + width: 100%; + } + `; + } } diff --git a/src/shared/editor/color-picker.ts b/src/shared/editor/color-picker.ts index a0679ca21..48190113b 100644 --- a/src/shared/editor/color-picker.ts +++ b/src/shared/editor/color-picker.ts @@ -8,80 +8,84 @@ import "./../form/mushroom-select"; @customElement("mushroom-color-picker") export class ColorPicker extends LitElement { - @property() public label = ""; + @property() public label = ""; - @property() public value?: string; + @property() public value?: string; - @property() public configValue = ""; + @property() public configValue = ""; - @property() public hass!: HomeAssistant; + @property() public hass!: HomeAssistant; - _selectChanged(ev) { - const value = ev.target.value; - if (value) { - this.dispatchEvent( - new CustomEvent("value-changed", { - detail: { - value: value !== "default" ? value : "", - }, - }) - ); - } + _selectChanged(ev) { + const value = ev.target.value; + if (value) { + this.dispatchEvent( + new CustomEvent("value-changed", { + detail: { + value: value !== "default" ? value : "", + }, + }) + ); } + } - render() { - const customLocalize = setupCustomlocalize(this.hass); + render() { + const customLocalize = setupCustomlocalize(this.hass); - return html` - e.stopPropagation()} - .value=${this.value || "default"} - fixedMenuPosition - naturalMenuWidth - > - ${this.renderColorCircle(this.value || "grey")} - - ${customLocalize("editor.form.color_picker.values.default")} - - ${COLORS.map( - (color) => html` - - ${computeColorName(color)} - ${this.renderColorCircle(color)} - - ` - )} - - `; - } + return html` + e.stopPropagation()} + .value=${this.value || "default"} + fixedMenuPosition + naturalMenuWidth + > + ${this.renderColorCircle(this.value || "grey")} + + ${customLocalize("editor.form.color_picker.values.default")} + + ${COLORS.map( + (color) => html` + + ${computeColorName(color)} + ${this.renderColorCircle(color)} + + ` + )} + + `; + } - private renderColorCircle(color: string) { - return html` - - `; - } + private renderColorCircle(color: string) { + return html` + + `; + } - static get styles(): CSSResultGroup { - return css` - mushroom-select { - width: 100%; - } - .circle-color { - display: block; - background-color: rgb(var(--main-color)); - border-radius: 10px; - width: 20px; - height: 20px; - } - `; - } + static get styles(): CSSResultGroup { + return css` + mushroom-select { + width: 100%; + } + .circle-color { + display: block; + background-color: rgb(var(--main-color)); + border-radius: 10px; + width: 20px; + height: 20px; + } + `; + } } diff --git a/src/shared/editor/icon-type-picker.ts b/src/shared/editor/icon-type-picker.ts index 19dce1156..f3653858e 100644 --- a/src/shared/editor/icon-type-picker.ts +++ b/src/shared/editor/icon-type-picker.ts @@ -7,64 +7,65 @@ import "../form/mushroom-select"; @customElement("mushroom-icon-type-picker") export class IconTypePicker extends LitElement { - @property() public label = ""; + @property() public label = ""; - @property() public value?: string; + @property() public value?: string; - @property() public configValue = ""; + @property() public configValue = ""; - @property() public hass!: HomeAssistant; + @property() public hass!: HomeAssistant; - _selectChanged(ev) { - const value = ev.target.value; - if (value) { - this.dispatchEvent( - new CustomEvent("value-changed", { - detail: { - value: value !== "default" ? value : "", - }, - }) - ); - } + _selectChanged(ev) { + const value = ev.target.value; + if (value) { + this.dispatchEvent( + new CustomEvent("value-changed", { + detail: { + value: value !== "default" ? value : "", + }, + }) + ); } + } - render() { - const customLocalize = setupCustomlocalize(this.hass); + render() { + const customLocalize = setupCustomlocalize(this.hass); - return html` - e.stopPropagation()} - .value=${this.value || "default"} - fixedMenuPosition - naturalMenuWidth - > - - ${customLocalize("editor.form.icon_type_picker.values.default")} - - ${ICON_TYPES.map((iconType) => { - return html` - - ${customLocalize(`editor.form.icon_type_picker.values.${iconType}`) || - capitalizeFirstLetter(iconType)} - - `; - })} - - `; - } + return html` + e.stopPropagation()} + .value=${this.value || "default"} + fixedMenuPosition + naturalMenuWidth + > + + ${customLocalize("editor.form.icon_type_picker.values.default")} + + ${ICON_TYPES.map((iconType) => { + return html` + + ${customLocalize( + `editor.form.icon_type_picker.values.${iconType}` + ) || capitalizeFirstLetter(iconType)} + + `; + })} + + `; + } - static get styles(): CSSResultGroup { - return css` - mushroom-select { - width: 100%; - } - `; - } + static get styles(): CSSResultGroup { + return css` + mushroom-select { + width: 100%; + } + `; + } } function capitalizeFirstLetter(string: string) { - return string.charAt(0).toUpperCase() + string.slice(1); + return string.charAt(0).toUpperCase() + string.slice(1); } diff --git a/src/shared/editor/info-picker.ts b/src/shared/editor/info-picker.ts index 70142abd1..f948d9454 100644 --- a/src/shared/editor/info-picker.ts +++ b/src/shared/editor/info-picker.ts @@ -7,66 +7,66 @@ import "./../form/mushroom-select"; @customElement("mushroom-info-picker") export class InfoPicker extends LitElement { - @property() public label = ""; + @property() public label = ""; - @property() public value?: string; + @property() public value?: string; - @property() public configValue = ""; + @property() public configValue = ""; - @property() public infos?: Info[]; + @property() public infos?: Info[]; - @property() public hass!: HomeAssistant; + @property() public hass!: HomeAssistant; - _selectChanged(ev) { - const value = ev.target.value; - if (value) { - this.dispatchEvent( - new CustomEvent("value-changed", { - detail: { - value: value !== "default" ? value : "", - }, - }) - ); - } + _selectChanged(ev) { + const value = ev.target.value; + if (value) { + this.dispatchEvent( + new CustomEvent("value-changed", { + detail: { + value: value !== "default" ? value : "", + }, + }) + ); } + } - render() { - const customLocalize = setupCustomlocalize(this.hass); + render() { + const customLocalize = setupCustomlocalize(this.hass); - return html` - e.stopPropagation()} - .value=${this.value || "default"} - fixedMenuPosition - naturalMenuWidth - > - - ${customLocalize("editor.form.info_picker.values.default")} - - ${(this.infos ?? INFOS).map((info) => { - return html` - - ${customLocalize(`editor.form.info_picker.values.${info}`) || - capitalizeFirstLetter(info)} - - `; - })} - - `; - } + return html` + e.stopPropagation()} + .value=${this.value || "default"} + fixedMenuPosition + naturalMenuWidth + > + + ${customLocalize("editor.form.info_picker.values.default")} + + ${(this.infos ?? INFOS).map((info) => { + return html` + + ${customLocalize(`editor.form.info_picker.values.${info}`) || + capitalizeFirstLetter(info)} + + `; + })} + + `; + } - static get styles(): CSSResultGroup { - return css` - mushroom-select { - width: 100%; - } - `; - } + static get styles(): CSSResultGroup { + return css` + mushroom-select { + width: 100%; + } + `; + } } function capitalizeFirstLetter(string: string) { - return string.charAt(0).toUpperCase() + string.slice(1); + return string.charAt(0).toUpperCase() + string.slice(1); } diff --git a/src/shared/editor/layout-picker.ts b/src/shared/editor/layout-picker.ts index 803daa8bd..dc8102ef0 100644 --- a/src/shared/editor/layout-picker.ts +++ b/src/shared/editor/layout-picker.ts @@ -8,68 +8,68 @@ const LAYOUTS = ["default", "horizontal", "vertical"] as const; type Layout = (typeof LAYOUTS)[number]; const ICONS: Record = { - default: "mdi:card-text-outline", - vertical: "mdi:focus-field-vertical", - horizontal: "mdi:focus-field-horizontal", + default: "mdi:card-text-outline", + vertical: "mdi:focus-field-vertical", + horizontal: "mdi:focus-field-horizontal", }; @customElement("mushroom-layout-picker") export class LayoutPicker extends LitElement { - @property() public label = ""; + @property() public label = ""; - @property() public value?: string; + @property() public value?: string; - @property() public configValue = ""; + @property() public configValue = ""; - @property() public hass!: HomeAssistant; + @property() public hass!: HomeAssistant; - _selectChanged(ev) { - const value = ev.target.value; - if (value) { - this.dispatchEvent( - new CustomEvent("value-changed", { - detail: { - value: value !== "default" ? value : "", - }, - }) - ); - } + _selectChanged(ev) { + const value = ev.target.value; + if (value) { + this.dispatchEvent( + new CustomEvent("value-changed", { + detail: { + value: value !== "default" ? value : "", + }, + }) + ); } + } - render() { - const customLocalize = setupCustomlocalize(this.hass); + render() { + const customLocalize = setupCustomlocalize(this.hass); - const value = this.value || "default"; + const value = this.value || "default"; - return html` - e.stopPropagation()} - .value=${value} - fixedMenuPosition - naturalMenuWidth - > - - ${LAYOUTS.map( - (layout) => html` - - ${customLocalize(`editor.form.layout_picker.values.${layout}`)} - - - ` - )} - - `; - } + return html` + e.stopPropagation()} + .value=${value} + fixedMenuPosition + naturalMenuWidth + > + + ${LAYOUTS.map( + (layout) => html` + + ${customLocalize(`editor.form.layout_picker.values.${layout}`)} + + + ` + )} + + `; + } - static get styles(): CSSResultGroup { - return css` - mushroom-select { - width: 100%; - } - `; - } + static get styles(): CSSResultGroup { + return css` + mushroom-select { + width: 100%; + } + `; + } } diff --git a/src/shared/form/mushroom-select.ts b/src/shared/form/mushroom-select.ts index 1fe8adbff..bc183a7e3 100644 --- a/src/shared/form/mushroom-select.ts +++ b/src/shared/form/mushroom-select.ts @@ -6,44 +6,49 @@ import { debounce, nextRender } from "../../ha"; @customElement("mushroom-select") export class MushroomSelect extends SelectBase { - // @ts-ignore - @property({ type: Boolean }) public icon?: boolean; + // @ts-ignore + @property({ type: Boolean }) public icon?: boolean; - protected override renderLeadingIcon() { - if (!this.icon) { - return nothing; - } - - return html``; - } - - connectedCallback() { - super.connectedCallback(); - window.addEventListener("translations-updated", this._translationsUpdated); + protected override renderLeadingIcon() { + if (!this.icon) { + return nothing; } - disconnectedCallback() { - super.disconnectedCallback(); - window.removeEventListener("translations-updated", this._translationsUpdated); - } - - private _translationsUpdated = debounce(async () => { - await nextRender(); - this.layoutOptions(); - }, 500); - - static override styles = [ - styles, - css` - .mdc-select__anchor { - height: var(--select-height, 56px) !important; - } - `, - ]; + return html``; + } + + connectedCallback() { + super.connectedCallback(); + window.addEventListener("translations-updated", this._translationsUpdated); + } + + disconnectedCallback() { + super.disconnectedCallback(); + window.removeEventListener( + "translations-updated", + this._translationsUpdated + ); + } + + private _translationsUpdated = debounce(async () => { + await nextRender(); + this.layoutOptions(); + }, 500); + + static override styles = [ + styles, + css` + .mdc-select__anchor { + height: var(--select-height, 56px) !important; + } + `, + ]; } declare global { - interface HTMLElementTagNameMap { - "mushroom-select": MushroomSelect; - } + interface HTMLElementTagNameMap { + "mushroom-select": MushroomSelect; + } } diff --git a/src/shared/form/mushroom-textfield.ts b/src/shared/form/mushroom-textfield.ts index 78a3b7779..095b4915f 100644 --- a/src/shared/form/mushroom-textfield.ts +++ b/src/shared/form/mushroom-textfield.ts @@ -4,78 +4,83 @@ import { property } from "lit/decorators.js"; import { TextFieldBase } from "@material/mwc-textfield/mwc-textfield-base"; class MushroomTextField extends TextFieldBase { - @property({ type: Boolean }) public invalid?: boolean; + @property({ type: Boolean }) public invalid?: boolean; - @property({ attribute: "error-message" }) public errorMessage?: string; + @property({ attribute: "error-message" }) public errorMessage?: string; - override updated(changedProperties: PropertyValues) { - super.updated(changedProperties); - if ( - (changedProperties.has("invalid") && - (this.invalid || changedProperties.get("invalid") !== undefined)) || - changedProperties.has("errorMessage") - ) { - this.setCustomValidity(this.invalid ? this.errorMessage || "Invalid" : ""); - this.reportValidity(); - } + override updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if ( + (changedProperties.has("invalid") && + (this.invalid || changedProperties.get("invalid") !== undefined)) || + changedProperties.has("errorMessage") + ) { + this.setCustomValidity( + this.invalid ? this.errorMessage || "Invalid" : "" + ); + this.reportValidity(); } + } - override renderOutline() { - return ""; - } + override renderOutline() { + return ""; + } - protected override renderIcon(_icon: string, isTrailingIcon = false): TemplateResult { - const type = isTrailingIcon ? "trailing" : "leading"; + protected override renderIcon( + _icon: string, + isTrailingIcon = false + ): TemplateResult { + const type = isTrailingIcon ? "trailing" : "leading"; - return html` - - - - `; - } + return html` + + + + `; + } - static override styles = [ - styles, - css` - .mdc-text-field__input { - width: var(--ha-textfield-input-width, 100%); - } - .mdc-text-field:not(.mdc-text-field--with-leading-icon) { - padding: var(--text-field-padding, 0px 16px); - } - .mdc-text-field__affix--suffix { - padding-left: var(--text-field-suffix-padding-left, 12px); - padding-right: var(--text-field-suffix-padding-right, 0px); - } + static override styles = [ + styles, + css` + .mdc-text-field__input { + width: var(--ha-textfield-input-width, 100%); + } + .mdc-text-field:not(.mdc-text-field--with-leading-icon) { + padding: var(--text-field-padding, 0px 16px); + } + .mdc-text-field__affix--suffix { + padding-left: var(--text-field-suffix-padding-left, 12px); + padding-right: var(--text-field-suffix-padding-right, 0px); + } - input { - text-align: var(--text-field-text-align); - } + input { + text-align: var(--text-field-text-align); + } - /* Chrome, Safari, Edge, Opera */ - :host([no-spinner]) input::-webkit-outer-spin-button, - :host([no-spinner]) input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } + /* Chrome, Safari, Edge, Opera */ + :host([no-spinner]) input::-webkit-outer-spin-button, + :host([no-spinner]) input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } - /* Firefox */ - :host([no-spinner]) input[type="number"] { - -moz-appearance: textfield; - } + /* Firefox */ + :host([no-spinner]) input[type="number"] { + -moz-appearance: textfield; + } - .mdc-text-field__ripple { - overflow: hidden; - } + .mdc-text-field__ripple { + overflow: hidden; + } - .mdc-text-field { - overflow: var(--text-field-overflow); - } - `, - ]; + .mdc-text-field { + overflow: var(--text-field-overflow); + } + `, + ]; } customElements.define("mushroom-textfield", MushroomTextField); diff --git a/src/shared/input-number.ts b/src/shared/input-number.ts index 07bd347ad..baa7eca12 100644 --- a/src/shared/input-number.ts +++ b/src/shared/input-number.ts @@ -1,204 +1,219 @@ -import { css, CSSResultGroup, html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; -import { conditionalClamp, debounce, formatNumber, FrontendLocaleData, round } from "../ha"; +import { + conditionalClamp, + debounce, + formatNumber, + FrontendLocaleData, + round, +} from "../ha"; const DEFAULT_STEP = 1; const DEFAULT_DEBOUCE_TIME = 2000; const getInputNumberDebounceTime = (element: any): number => { - const debounceTimeValue = window - .getComputedStyle(element) - .getPropertyValue("--input-number-debounce"); - const debounceTime = parseFloat(debounceTimeValue); - return isNaN(debounceTime) ? DEFAULT_DEBOUCE_TIME : debounceTime; + const debounceTimeValue = window + .getComputedStyle(element) + .getPropertyValue("--input-number-debounce"); + const debounceTime = parseFloat(debounceTimeValue); + return isNaN(debounceTime) ? DEFAULT_DEBOUCE_TIME : debounceTime; }; @customElement("mushroom-input-number") export class InputNumber extends LitElement { - @property({ attribute: false }) public locale!: FrontendLocaleData; - - @property({ type: Boolean }) public disabled: boolean = false; - - @property({ attribute: false, type: Number, reflect: true }) - public value?: number; - - @property({ type: Number }) - public step?: number; - - @property({ type: Number }) - public min?: number; - - @property({ type: Number }) - public max?: number; - - @property({ attribute: "false" }) - public formatOptions: Intl.NumberFormatOptions = {}; - - @state() pending = false; - - private get _precision() { - return Math.ceil(Math.log10(1 / this._step)); - } - - private get _step() { - return this.step ?? DEFAULT_STEP; - } - - private _incrementValue(e: MouseEvent) { - e.stopPropagation(); - if (this.value == null) return; - const value = round(this.value + this._step, this._precision); - this._processNewValue(value); - } - - private _decrementValue(e: MouseEvent) { - e.stopPropagation(); - if (this.value == null) return; - const value = round(this.value - this._step, this._precision); - this._processNewValue(value); - } - - @query("#container") - private container; - - private dispatchValue = (value: number) => { - this.pending = false; - this.dispatchEvent( - new CustomEvent("change", { - detail: { - value, - }, - }) - ); - }; - - private debounceDispatchValue = this.dispatchValue; - - protected firstUpdated(changedProperties: PropertyValues): void { - super.firstUpdated(changedProperties); - const debounceTime = getInputNumberDebounceTime(this.container); - if (debounceTime) { - this.debounceDispatchValue = debounce(this.dispatchValue, debounceTime); - } - } - - private _processNewValue(value) { - const newValue = conditionalClamp(value, this.min, this.max); - if (this.value !== newValue) { - this.value = newValue; - this.pending = true; - } - this.debounceDispatchValue(newValue); - } - - protected render(): TemplateResult { - const value = - this.value != null ? formatNumber(this.value, this.locale, this.formatOptions) : "-"; - - return html` -
- - - ${value} - - -
- `; + @property({ attribute: false }) public locale!: FrontendLocaleData; + + @property({ type: Boolean }) public disabled: boolean = false; + + @property({ attribute: false, type: Number, reflect: true }) + public value?: number; + + @property({ type: Number }) + public step?: number; + + @property({ type: Number }) + public min?: number; + + @property({ type: Number }) + public max?: number; + + @property({ attribute: "false" }) + public formatOptions: Intl.NumberFormatOptions = {}; + + @state() pending = false; + + private get _precision() { + return Math.ceil(Math.log10(1 / this._step)); + } + + private get _step() { + return this.step ?? DEFAULT_STEP; + } + + private _incrementValue(e: MouseEvent) { + e.stopPropagation(); + if (this.value == null) return; + const value = round(this.value + this._step, this._precision); + this._processNewValue(value); + } + + private _decrementValue(e: MouseEvent) { + e.stopPropagation(); + if (this.value == null) return; + const value = round(this.value - this._step, this._precision); + this._processNewValue(value); + } + + @query("#container") + private container; + + private dispatchValue = (value: number) => { + this.pending = false; + this.dispatchEvent( + new CustomEvent("change", { + detail: { + value, + }, + }) + ); + }; + + private debounceDispatchValue = this.dispatchValue; + + protected firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + const debounceTime = getInputNumberDebounceTime(this.container); + if (debounceTime) { + this.debounceDispatchValue = debounce(this.dispatchValue, debounceTime); } + } - static get styles(): CSSResultGroup { - return css` - :host { - --text-color: var(--primary-text-color); - --text-color-disabled: rgb(var(--rgb-disabled)); - --icon-color: var(--primary-text-color); - --icon-color-disabled: rgb(var(--rgb-disabled)); - --bg-color: rgba(var(--rgb-primary-text-color), 0.05); - --bg-color-disabled: rgba(var(--rgb-disabled), 0.2); - height: var(--control-height); - width: calc(var(--control-height) * var(--control-button-ratio) * 3); - flex: none; - } - .container { - box-sizing: border-box; - width: 100%; - height: 100%; - padding: 6px; - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - border-radius: var(--control-border-radius); - border: none; - background-color: var(--bg-color); - transition: background-color 280ms ease-in-out; - height: var(--control-height); - overflow: hidden; - } - .button { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - padding: 4px; - border: none; - background: none; - cursor: pointer; - border-radius: var(--control-border-radius); - line-height: 0; - height: 100%; - } - .minus { - padding-right: 0; - } - .plus { - padding-left: 0; - } - .button:disabled { - cursor: not-allowed; - } - .button ha-icon { - font-size: var(--control-height); - --mdc-icon-size: var(--control-icon-size); - color: var(--icon-color); - pointer-events: none; - } - .button:disabled ha-icon { - color: var(--icon-color-disabled); - } - .value { - text-align: center; - flex-grow: 1; - flex-shrink: 0; - flex-basis: 20px; - font-weight: bold; - color: var(--text-color); - } - .value.disabled { - color: var(--text-color-disabled); - } - .value.pending { - opacity: 0.5; - } - `; + private _processNewValue(value) { + const newValue = conditionalClamp(value, this.min, this.max); + if (this.value !== newValue) { + this.value = newValue; + this.pending = true; } + this.debounceDispatchValue(newValue); + } + + protected render(): TemplateResult { + const value = + this.value != null + ? formatNumber(this.value, this.locale, this.formatOptions) + : "-"; + + return html` +
+ + + ${value} + + +
+ `; + } + + static get styles(): CSSResultGroup { + return css` + :host { + --text-color: var(--primary-text-color); + --text-color-disabled: rgb(var(--rgb-disabled)); + --icon-color: var(--primary-text-color); + --icon-color-disabled: rgb(var(--rgb-disabled)); + --bg-color: rgba(var(--rgb-primary-text-color), 0.05); + --bg-color-disabled: rgba(var(--rgb-disabled), 0.2); + height: var(--control-height); + width: calc(var(--control-height) * var(--control-button-ratio) * 3); + flex: none; + } + .container { + box-sizing: border-box; + width: 100%; + height: 100%; + padding: 6px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + border-radius: var(--control-border-radius); + border: none; + background-color: var(--bg-color); + transition: background-color 280ms ease-in-out; + height: var(--control-height); + overflow: hidden; + } + .button { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding: 4px; + border: none; + background: none; + cursor: pointer; + border-radius: var(--control-border-radius); + line-height: 0; + height: 100%; + } + .minus { + padding-right: 0; + } + .plus { + padding-left: 0; + } + .button:disabled { + cursor: not-allowed; + } + .button ha-icon { + font-size: var(--control-height); + --mdc-icon-size: var(--control-icon-size); + color: var(--icon-color); + pointer-events: none; + } + .button:disabled ha-icon { + color: var(--icon-color-disabled); + } + .value { + text-align: center; + flex-grow: 1; + flex-shrink: 0; + flex-basis: 20px; + font-weight: bold; + color: var(--text-color); + } + .value.disabled { + color: var(--text-color-disabled); + } + .value.pending { + opacity: 0.5; + } + `; + } } diff --git a/src/shared/shape-avatar.ts b/src/shared/shape-avatar.ts index 3c79bd3eb..a4dd60182 100644 --- a/src/shared/shape-avatar.ts +++ b/src/shared/shape-avatar.ts @@ -4,39 +4,39 @@ import { classMap } from "lit/directives/class-map.js"; @customElement("mushroom-shape-avatar") export class ShapePicture extends LitElement { - @property() public picture_url: string = ""; + @property() public picture_url: string = ""; - protected render(): TemplateResult { - return html` -
- -
- `; - } + protected render(): TemplateResult { + return html` +
+ +
+ `; + } - static get styles(): CSSResultGroup { - return css` - :host { - --main-color: var(--primary-text-color); - --icon-color-disabled: rgb(var(--rgb-disabled)); - --shape-color: rgba(var(--rgb-primary-text-color), 0.05); - --shape-color-disabled: rgba(var(--rgb-disabled), 0.2); - flex: none; - } - .container { - position: relative; - width: var(--icon-size); - height: var(--icon-size); - flex: none; - display: flex; - align-items: center; - justify-content: center; - } - .picture { - width: 100%; - height: 100%; - border-radius: var(--icon-border-radius); - } - `; - } + static get styles(): CSSResultGroup { + return css` + :host { + --main-color: var(--primary-text-color); + --icon-color-disabled: rgb(var(--rgb-disabled)); + --shape-color: rgba(var(--rgb-primary-text-color), 0.05); + --shape-color-disabled: rgba(var(--rgb-disabled), 0.2); + flex: none; + } + .container { + position: relative; + width: var(--icon-size); + height: var(--icon-size); + flex: none; + display: flex; + align-items: center; + justify-content: center; + } + .picture { + width: 100%; + height: 100%; + border-radius: var(--icon-border-radius); + } + `; + } } diff --git a/src/shared/shape-icon.ts b/src/shared/shape-icon.ts index 3e2350e75..4f3974dc4 100644 --- a/src/shared/shape-icon.ts +++ b/src/shared/shape-icon.ts @@ -5,70 +5,70 @@ import { animations } from "../utils/entity-styles"; @customElement("mushroom-shape-icon") export class ShapeIcon extends LitElement { - @property({ type: Boolean }) public disabled?: boolean; + @property({ type: Boolean }) public disabled?: boolean; - protected render(): TemplateResult { - return html` -
- -
- `; - } + protected render(): TemplateResult { + return html` +
+ +
+ `; + } - static get styles(): CSSResultGroup { - return [ - // animations must be on this element for safari - animations, - css` - :host { - --icon-color: var(--primary-text-color); - --icon-color-disabled: rgb(var(--rgb-disabled)); - --shape-color: rgba(var(--rgb-primary-text-color), 0.05); - --shape-color-disabled: rgba(var(--rgb-disabled), 0.2); - --shape-animation: none; - --shape-outline-color: transparent; - flex: none; - } - .shape { - position: relative; - width: var(--icon-size); - height: var(--icon-size); - font-size: var(--icon-size); - border-radius: var(--icon-border-radius); - display: flex; - align-items: center; - justify-content: center; - background-color: var(--shape-color); - transition-property: background-color, box-shadow; - transition-duration: 280ms; - transition-timing-function: ease-out; - animation: var(--shape-animation); - box-shadow: 0 0 0 1px var(--shape-outline-color); - } + static get styles(): CSSResultGroup { + return [ + // animations must be on this element for safari + animations, + css` + :host { + --icon-color: var(--primary-text-color); + --icon-color-disabled: rgb(var(--rgb-disabled)); + --shape-color: rgba(var(--rgb-primary-text-color), 0.05); + --shape-color-disabled: rgba(var(--rgb-disabled), 0.2); + --shape-animation: none; + --shape-outline-color: transparent; + flex: none; + } + .shape { + position: relative; + width: var(--icon-size); + height: var(--icon-size); + font-size: var(--icon-size); + border-radius: var(--icon-border-radius); + display: flex; + align-items: center; + justify-content: center; + background-color: var(--shape-color); + transition-property: background-color, box-shadow; + transition-duration: 280ms; + transition-timing-function: ease-out; + animation: var(--shape-animation); + box-shadow: 0 0 0 1px var(--shape-outline-color); + } - .shape ::slotted(*) { - display: flex; - color: var(--icon-color); - transition: color 280ms ease-in-out; - } - ::slotted(ha-icon), - ::slotted(ha-state-icon) { - display: flex; - line-height: 0; - --mdc-icon-size: var(--icon-symbol-size); - } - .shape.disabled { - background-color: var(--shape-color-disabled); - } - .shape.disabled ::slotted(*) { - color: var(--icon-color-disabled); - } - `, - ]; - } + .shape ::slotted(*) { + display: flex; + color: var(--icon-color); + transition: color 280ms ease-in-out; + } + ::slotted(ha-icon), + ::slotted(ha-state-icon) { + display: flex; + line-height: 0; + --mdc-icon-size: var(--icon-symbol-size); + } + .shape.disabled { + background-color: var(--shape-color-disabled); + } + .shape.disabled ::slotted(*) { + color: var(--icon-color-disabled); + } + `, + ]; + } } diff --git a/src/shared/slider.ts b/src/shared/slider.ts index 8a77048b8..5a7221c2f 100644 --- a/src/shared/slider.ts +++ b/src/shared/slider.ts @@ -1,11 +1,11 @@ import { - css, - CSSResultGroup, - html, - LitElement, - nothing, - PropertyValues, - TemplateResult, + css, + CSSResultGroup, + html, + LitElement, + nothing, + PropertyValues, + TemplateResult, } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; @@ -13,268 +13,276 @@ import { styleMap } from "lit/directives/style-map.js"; import "hammerjs"; const getPercentageFromEvent = (e: HammerInput) => { - const x = e.center.x; - const offset = e.target.getBoundingClientRect().left; - const total = e.target.clientWidth; - return Math.max(Math.min(1, (x - offset) / total), 0); + const x = e.center.x; + const offset = e.target.getBoundingClientRect().left; + const total = e.target.clientWidth; + return Math.max(Math.min(1, (x - offset) / total), 0); }; export const DEFAULT_SLIDER_THRESHOLD = 10; const getSliderThreshold = (element: any): number | undefined => { - const thresholdValue = window.getComputedStyle(element).getPropertyValue("--slider-threshold"); - const threshold = parseFloat(thresholdValue); - return isNaN(threshold) ? DEFAULT_SLIDER_THRESHOLD : threshold; + const thresholdValue = window + .getComputedStyle(element) + .getPropertyValue("--slider-threshold"); + const threshold = parseFloat(thresholdValue); + return isNaN(threshold) ? DEFAULT_SLIDER_THRESHOLD : threshold; }; @customElement("mushroom-slider") export class SliderItem extends LitElement { - @property({ type: Boolean }) public disabled: boolean = false; + @property({ type: Boolean }) public disabled: boolean = false; - @property({ type: Boolean }) public inactive: boolean = false; + @property({ type: Boolean }) public inactive: boolean = false; - @property({ type: Boolean, attribute: "show-active" }) - public showActive?: boolean; + @property({ type: Boolean, attribute: "show-active" }) + public showActive?: boolean; - @property({ type: Boolean, attribute: "show-indicator" }) - public showIndicator?: boolean; + @property({ type: Boolean, attribute: "show-indicator" }) + public showIndicator?: boolean; - @property({ attribute: false, type: Number, reflect: true }) - public value?: number; + @property({ attribute: false, type: Number, reflect: true }) + public value?: number; - @property({ type: Number }) - public step: number = 1; + @property({ type: Number }) + public step: number = 1; - @property({ type: Number }) - public min: number = 0; + @property({ type: Number }) + public min: number = 0; - @property({ type: Number }) - public max: number = 100; + @property({ type: Number }) + public max: number = 100; - private _mc?: HammerManager; + private _mc?: HammerManager; - @state() controlled: boolean = false; + @state() controlled: boolean = false; - valueToPercentage(value: number) { - return (value - this.min) / (this.max - this.min); - } + valueToPercentage(value: number) { + return (value - this.min) / (this.max - this.min); + } - percentageToValue(value: number) { - return (this.max - this.min) * value + this.min; - } + percentageToValue(value: number) { + return (this.max - this.min) * value + this.min; + } - protected firstUpdated(changedProperties: PropertyValues): void { - super.firstUpdated(changedProperties); - this.setupListeners(); - } + protected firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + this.setupListeners(); + } - connectedCallback(): void { - super.connectedCallback(); - this.setupListeners(); - } + connectedCallback(): void { + super.connectedCallback(); + this.setupListeners(); + } - disconnectedCallback(): void { - super.disconnectedCallback(); - this.destroyListeners(); - } + disconnectedCallback(): void { + super.disconnectedCallback(); + this.destroyListeners(); + } - @query("#slider") - private slider; + @query("#slider") + private slider; - setupListeners() { - if (this.slider && !this._mc) { - const threshold = getSliderThreshold(this.slider); - this._mc = new Hammer.Manager(this.slider, { touchAction: "pan-y" }); - this._mc.add( - new Hammer.Pan({ - threshold, - direction: Hammer.DIRECTION_ALL, - enable: true, - }) - ); + setupListeners() { + if (this.slider && !this._mc) { + const threshold = getSliderThreshold(this.slider); + this._mc = new Hammer.Manager(this.slider, { touchAction: "pan-y" }); + this._mc.add( + new Hammer.Pan({ + threshold, + direction: Hammer.DIRECTION_ALL, + enable: true, + }) + ); - this._mc.add(new Hammer.Tap({ event: "singletap" })); + this._mc.add(new Hammer.Tap({ event: "singletap" })); - let savedValue; - this._mc.on("panstart", () => { - if (this.disabled) return; - this.controlled = true; - savedValue = this.value; - }); - this._mc.on("pancancel", () => { - if (this.disabled) return; - this.controlled = false; - this.value = savedValue; - }); - this._mc.on("panmove", (e) => { - if (this.disabled) return; - const percentage = getPercentageFromEvent(e); - this.value = this.percentageToValue(percentage); - this.dispatchEvent( - new CustomEvent("current-change", { - detail: { - value: Math.round(this.value / this.step) * this.step, - }, - }) - ); - }); - this._mc.on("panend", (e) => { - if (this.disabled) return; - this.controlled = false; - const percentage = getPercentageFromEvent(e); - // Prevent from input releasing on a value that doesn't lie on a step - this.value = Math.round(this.percentageToValue(percentage) / this.step) * this.step; - this.dispatchEvent( - new CustomEvent("current-change", { - detail: { - value: undefined, - }, - }) - ); - this.dispatchEvent( - new CustomEvent("change", { - detail: { - value: this.value, - }, - }) - ); - }); + let savedValue; + this._mc.on("panstart", () => { + if (this.disabled) return; + this.controlled = true; + savedValue = this.value; + }); + this._mc.on("pancancel", () => { + if (this.disabled) return; + this.controlled = false; + this.value = savedValue; + }); + this._mc.on("panmove", (e) => { + if (this.disabled) return; + const percentage = getPercentageFromEvent(e); + this.value = this.percentageToValue(percentage); + this.dispatchEvent( + new CustomEvent("current-change", { + detail: { + value: Math.round(this.value / this.step) * this.step, + }, + }) + ); + }); + this._mc.on("panend", (e) => { + if (this.disabled) return; + this.controlled = false; + const percentage = getPercentageFromEvent(e); + // Prevent from input releasing on a value that doesn't lie on a step + this.value = + Math.round(this.percentageToValue(percentage) / this.step) * + this.step; + this.dispatchEvent( + new CustomEvent("current-change", { + detail: { + value: undefined, + }, + }) + ); + this.dispatchEvent( + new CustomEvent("change", { + detail: { + value: this.value, + }, + }) + ); + }); - this._mc.on("singletap", (e) => { - if (this.disabled) return; - const percentage = getPercentageFromEvent(e); - // Prevent from input selecting a value that doesn't lie on a step - this.value = Math.round(this.percentageToValue(percentage) / this.step) * this.step; - this.dispatchEvent( - new CustomEvent("change", { - detail: { - value: this.value, - }, - }) - ); - }); - } + this._mc.on("singletap", (e) => { + if (this.disabled) return; + const percentage = getPercentageFromEvent(e); + // Prevent from input selecting a value that doesn't lie on a step + this.value = + Math.round(this.percentageToValue(percentage) / this.step) * + this.step; + this.dispatchEvent( + new CustomEvent("change", { + detail: { + value: this.value, + }, + }) + ); + }); } + } - destroyListeners() { - if (this._mc) { - this._mc.destroy(); - this._mc = undefined; - } + destroyListeners() { + if (this._mc) { + this._mc.destroy(); + this._mc = undefined; } + } - protected render(): TemplateResult { - return html` -
-
-
- ${this.showActive ? html`
` : nothing} - ${this.showIndicator - ? html`
` - : nothing} -
-
- `; - } + protected render(): TemplateResult { + return html` +
+
+
+ ${this.showActive + ? html`
` + : nothing} + ${this.showIndicator + ? html`
` + : nothing} +
+
+ `; + } - static get styles(): CSSResultGroup { - return css` - :host { - --main-color: rgba(var(--rgb-secondary-text-color), 1); - --bg-gradient: none; - --bg-color: rgba(var(--rgb-secondary-text-color), 0.2); - --main-color-inactive: rgb(var(--rgb-disabled)); - --bg-color-inactive: rgba(var(--rgb-disabled), 0.2); - } - .container { - display: flex; - flex-direction: row; - height: var(--control-height); - } - .slider { - position: relative; - height: 100%; - width: 100%; - border-radius: var(--control-border-radius); - transform: translateZ(0); - overflow: hidden; - cursor: pointer; - } - .slider * { - pointer-events: none; - } - .slider .slider-track-background { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - background-color: var(--bg-color); - background-image: var(--gradient); - } - .slider .slider-track-active { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - transform: scale3d(var(--value, 0), 1, 1); - transform-origin: left; - background-color: var(--main-color); - transition: transform 180ms ease-in-out; - } - .slider .slider-track-indicator { - position: absolute; - top: 0; - bottom: 0; - left: calc(var(--value, 0) * (100% - 10px)); - width: 10px; - border-radius: 3px; - background-color: white; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); - transition: left 180ms ease-in-out; - } - .slider .slider-track-indicator:after { - display: block; - content: ""; - background-color: var(--main-color); - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - margin: auto; - height: 20px; - width: 2px; - border-radius: 1px; - } - .inactive .slider .slider-track-background { - background-color: var(--bg-color-inactive); - background-image: none; - } - .inactive .slider .slider-track-indicator:after { - background-color: var(--main-color-inactive); - } - .inactive .slider .slider-track-active { - background-color: var(--main-color-inactive); - } - .controlled .slider .slider-track-active { - transition: none; - } - .controlled .slider .slider-track-indicator { - transition: none; - } - `; - } + static get styles(): CSSResultGroup { + return css` + :host { + --main-color: rgba(var(--rgb-secondary-text-color), 1); + --bg-gradient: none; + --bg-color: rgba(var(--rgb-secondary-text-color), 0.2); + --main-color-inactive: rgb(var(--rgb-disabled)); + --bg-color-inactive: rgba(var(--rgb-disabled), 0.2); + } + .container { + display: flex; + flex-direction: row; + height: var(--control-height); + } + .slider { + position: relative; + height: 100%; + width: 100%; + border-radius: var(--control-border-radius); + transform: translateZ(0); + overflow: hidden; + cursor: pointer; + } + .slider * { + pointer-events: none; + } + .slider .slider-track-background { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + background-color: var(--bg-color); + background-image: var(--gradient); + } + .slider .slider-track-active { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + transform: scale3d(var(--value, 0), 1, 1); + transform-origin: left; + background-color: var(--main-color); + transition: transform 180ms ease-in-out; + } + .slider .slider-track-indicator { + position: absolute; + top: 0; + bottom: 0; + left: calc(var(--value, 0) * (100% - 10px)); + width: 10px; + border-radius: 3px; + background-color: white; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); + transition: left 180ms ease-in-out; + } + .slider .slider-track-indicator:after { + display: block; + content: ""; + background-color: var(--main-color); + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + margin: auto; + height: 20px; + width: 2px; + border-radius: 1px; + } + .inactive .slider .slider-track-background { + background-color: var(--bg-color-inactive); + background-image: none; + } + .inactive .slider .slider-track-indicator:after { + background-color: var(--main-color-inactive); + } + .inactive .slider .slider-track-active { + background-color: var(--main-color-inactive); + } + .controlled .slider .slider-track-active { + transition: none; + } + .controlled .slider .slider-track-indicator { + transition: none; + } + `; + } } diff --git a/src/shared/state-info.ts b/src/shared/state-info.ts index 28613bc98..552e17e18 100644 --- a/src/shared/state-info.ts +++ b/src/shared/state-info.ts @@ -1,59 +1,68 @@ -import { css, CSSResultGroup, html, LitElement, nothing, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + nothing, + TemplateResult, +} from "lit"; import { customElement, property } from "lit/decorators.js"; @customElement("mushroom-state-info") export class StateItem extends LitElement { - @property({ attribute: false }) public primary?: string | TemplateResult<1>; + @property({ attribute: false }) public primary?: string | TemplateResult<1>; - @property({ attribute: false }) public secondary?: string | TemplateResult<1>; + @property({ attribute: false }) public secondary?: string | TemplateResult<1>; - @property({ type: Boolean }) public multiline_secondary?: boolean = false; + @property({ type: Boolean }) public multiline_secondary?: boolean = false; - protected render(): TemplateResult { - return html` -
- ${this.primary ?? ""} - ${this.secondary - ? html`${this.secondary}` - : nothing} -
- `; - } + protected render(): TemplateResult { + return html` +
+ ${this.primary ?? ""} + ${this.secondary + ? html`${this.secondary}` + : nothing} +
+ `; + } - static get styles(): CSSResultGroup { - return css` - .container { - min-width: 0; - flex: 1; - display: flex; - flex-direction: column; - } - .primary { - font-weight: var(--card-primary-font-weight); - font-size: var(--card-primary-font-size); - line-height: var(--card-primary-line-height); - color: var(--card-primary-color); - letter-spacing: var(--card-primary-letter-spacing); - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - .secondary { - font-weight: var(--card-secondary-font-weight); - font-size: var(--card-secondary-font-size); - line-height: var(--card-secondary-line-height); - color: var(--card-secondary-color); - letter-spacing: var(--card-secondary-letter-spacing); - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - .multiline_secondary { - white-space: pre-wrap; - } - `; - } + static get styles(): CSSResultGroup { + return css` + .container { + min-width: 0; + flex: 1; + display: flex; + flex-direction: column; + } + .primary { + font-weight: var(--card-primary-font-weight); + font-size: var(--card-primary-font-size); + line-height: var(--card-primary-line-height); + color: var(--card-primary-color); + letter-spacing: var(--card-primary-letter-spacing); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + .secondary { + font-weight: var(--card-secondary-font-weight); + font-size: var(--card-secondary-font-size); + line-height: var(--card-secondary-line-height); + color: var(--card-secondary-color); + letter-spacing: var(--card-secondary-letter-spacing); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + .multiline_secondary { + white-space: pre-wrap; + } + `; + } } diff --git a/src/shared/state-item.ts b/src/shared/state-item.ts index 6e43fadf3..f62db4c0d 100644 --- a/src/shared/state-item.ts +++ b/src/shared/state-item.ts @@ -1,4 +1,11 @@ -import { css, CSSResultGroup, html, LitElement, nothing, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + nothing, + TemplateResult, +} from "lit"; import { customElement, property } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { Appearance } from "./config/appearance-config"; @@ -6,84 +13,84 @@ import "./shape-icon"; @customElement("mushroom-state-item") export class StateItem extends LitElement { - @property() public appearance?: Appearance; + @property() public appearance?: Appearance; - protected render(): TemplateResult { - return html` -
- ${this.appearance?.icon_type !== "none" - ? html` -
- - -
- ` - : nothing} - ${this.appearance?.primary_info !== "none" || - this.appearance?.secondary_info !== "none" - ? html` -
- -
- ` - : nothing} -
- `; - } + protected render(): TemplateResult { + return html` +
+ ${this.appearance?.icon_type !== "none" + ? html` +
+ + +
+ ` + : nothing} + ${this.appearance?.primary_info !== "none" || + this.appearance?.secondary_info !== "none" + ? html` +
+ +
+ ` + : nothing} +
+ `; + } - static get styles(): CSSResultGroup { - return css` - .container { - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; - } - .container > *:not(:last-child) { - margin-right: var(--spacing); - } - :host([rtl]) .container > *:not(:last-child) { - margin-right: initial; - margin-left: var(--spacing); - } - .icon { - position: relative; - } - .icon ::slotted(*[slot="badge"]) { - position: absolute; - top: -3px; - right: -3px; - } - :host([rtl]) .icon ::slotted(*[slot="badge"]) { - right: initial; - left: -3px; - } - .info { - min-width: 0; - width: 100%; - display: flex; - flex-direction: column; - } - .container.vertical { - flex-direction: column; - } - .container.vertical > *:not(:last-child) { - margin-bottom: var(--spacing); - margin-right: 0; - margin-left: 0; - } - :host([rtl]) .container.vertical > *:not(:last-child) { - margin-right: initial; - margin-left: initial; - } - .container.vertical .info { - text-align: center; - } - `; - } + static get styles(): CSSResultGroup { + return css` + .container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + } + .container > *:not(:last-child) { + margin-right: var(--spacing); + } + :host([rtl]) .container > *:not(:last-child) { + margin-right: initial; + margin-left: var(--spacing); + } + .icon { + position: relative; + } + .icon ::slotted(*[slot="badge"]) { + position: absolute; + top: -3px; + right: -3px; + } + :host([rtl]) .icon ::slotted(*[slot="badge"]) { + right: initial; + left: -3px; + } + .info { + min-width: 0; + width: 100%; + display: flex; + flex-direction: column; + } + .container.vertical { + flex-direction: column; + } + .container.vertical > *:not(:last-child) { + margin-bottom: var(--spacing); + margin-right: 0; + margin-left: 0; + } + :host([rtl]) .container.vertical > *:not(:last-child) { + margin-right: initial; + margin-left: initial; + } + .container.vertical .info { + text-align: center; + } + `; + } } diff --git a/src/utils/appearance.ts b/src/utils/appearance.ts index fb02d4931..7babf3659 100644 --- a/src/utils/appearance.ts +++ b/src/utils/appearance.ts @@ -1,46 +1,51 @@ -import { Appearance, AppearanceSharedConfig } from "../shared/config/appearance-config"; +import { + Appearance, + AppearanceSharedConfig, +} from "../shared/config/appearance-config"; import { IconType, Info } from "./info"; import { Layout } from "./layout"; type AdditionalConfig = { [key: string]: any }; -export function computeAppearance(config: AppearanceSharedConfig & AdditionalConfig): Appearance { - return { - layout: config.layout ?? getDefaultLayout(config), - fill_container: config.fill_container ?? false, - primary_info: config.primary_info ?? getDefaultPrimaryInfo(config), - secondary_info: config.secondary_info ?? getDefaultSecondaryInfo(config), - icon_type: config.icon_type ?? getDefaultIconType(config), - }; +export function computeAppearance( + config: AppearanceSharedConfig & AdditionalConfig +): Appearance { + return { + layout: config.layout ?? getDefaultLayout(config), + fill_container: config.fill_container ?? false, + primary_info: config.primary_info ?? getDefaultPrimaryInfo(config), + secondary_info: config.secondary_info ?? getDefaultSecondaryInfo(config), + icon_type: config.icon_type ?? getDefaultIconType(config), + }; } function getDefaultLayout(config: AdditionalConfig): Layout { - if (config.vertical) { - return "vertical"; - } - return "default"; + if (config.vertical) { + return "vertical"; + } + return "default"; } function getDefaultIconType(config: AdditionalConfig): IconType { - if (config.hide_icon) { - return "none"; - } - if (config.use_entity_picture || config.use_media_artwork) { - return "entity-picture"; - } - return "icon"; + if (config.hide_icon) { + return "none"; + } + if (config.use_entity_picture || config.use_media_artwork) { + return "entity-picture"; + } + return "icon"; } function getDefaultPrimaryInfo(config: AdditionalConfig): Info { - if (config.hide_name) { - return "none"; - } - return "name"; + if (config.hide_name) { + return "none"; + } + return "name"; } function getDefaultSecondaryInfo(config: AdditionalConfig): Info { - if (config.hide_state) { - return "none"; - } - return "state"; + if (config.hide_state) { + return "none"; + } + return "state"; } diff --git a/src/utils/base-card.ts b/src/utils/base-card.ts index 9490cb4c9..0276a0cc8 100644 --- a/src/utils/base-card.ts +++ b/src/utils/base-card.ts @@ -3,17 +3,20 @@ import { html, nothing, TemplateResult } from "lit"; import { property, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import { - computeRTL, - computeStateDisplay, - HomeAssistant, - isActive, - isAvailable, - LovelaceLayoutOptions, + computeRTL, + computeStateDisplay, + HomeAssistant, + isActive, + isAvailable, + LovelaceLayoutOptions, } from "../ha"; import setupCustomlocalize from "../localize"; import "../shared/badge-icon"; import "../shared/card"; -import { Appearance, AppearanceSharedConfig } from "../shared/config/appearance-config"; +import { + Appearance, + AppearanceSharedConfig, +} from "../shared/config/appearance-config"; import { EntitySharedConfig } from "../shared/config/entity-config"; import "../shared/shape-avatar"; import "../shared/shape-icon"; @@ -26,191 +29,196 @@ import { computeInfoDisplay } from "./info"; type BaseConfig = EntitySharedConfig & AppearanceSharedConfig; export function computeDarkMode(hass?: HomeAssistant): boolean { - if (!hass) return false; - return (hass.themes as any).darkMode as boolean; + if (!hass) return false; + return (hass.themes as any).darkMode as boolean; } export class MushroomBaseCard< - T extends BaseConfig = BaseConfig, - E extends HassEntity = HassEntity, + T extends BaseConfig = BaseConfig, + E extends HassEntity = HassEntity, > extends MushroomBaseElement { - @state() protected _config?: T; - - @property({ reflect: true, type: String }) - public layout: string | undefined; - - // For backward compatibility (version < 2024.7) - @property({ attribute: "in-grid", reflect: true, type: Boolean }) - protected _inGrid = false; - - protected get _stateObj(): E | undefined { - if (!this._config || !this.hass || !this._config.entity) return undefined; - - const entityId = this._config.entity; - return this.hass.states[entityId] as E; - } - - protected get hasControls(): boolean { - return false; - } - - setConfig(config: T): void { - this._config = { - tap_action: { - action: "more-info", - }, - hold_action: { - action: "more-info", - }, - ...config, - }; - } - - // For backward compatibility - public getGridSize(): [number | undefined, number | undefined] { - const { grid_columns, grid_rows } = this.getLayoutOptions(); - return [grid_columns, grid_rows]; - } - - public getCardSize(): number | Promise { - let height = 1; - if (!this._config) return height; - const appearance = computeAppearance(this._config); - if (appearance.layout === "vertical") { - height += 1; - } - if ( - appearance?.layout !== "horizontal" && - this.hasControls && - !("collapsible_controls" in this._config && this._config?.collapsible_controls) - ) { - height += 1; - } - return height; + @state() protected _config?: T; + + @property({ reflect: true, type: String }) + public layout: string | undefined; + + // For backward compatibility (version < 2024.7) + @property({ attribute: "in-grid", reflect: true, type: Boolean }) + protected _inGrid = false; + + protected get _stateObj(): E | undefined { + if (!this._config || !this.hass || !this._config.entity) return undefined; + + const entityId = this._config.entity; + return this.hass.states[entityId] as E; + } + + protected get hasControls(): boolean { + return false; + } + + setConfig(config: T): void { + this._config = { + tap_action: { + action: "more-info", + }, + hold_action: { + action: "more-info", + }, + ...config, + }; + } + + // For backward compatibility + public getGridSize(): [number | undefined, number | undefined] { + const { grid_columns, grid_rows } = this.getLayoutOptions(); + return [grid_columns, grid_rows]; + } + + public getCardSize(): number | Promise { + let height = 1; + if (!this._config) return height; + const appearance = computeAppearance(this._config); + if (appearance.layout === "vertical") { + height += 1; } - - public getLayoutOptions(): LovelaceLayoutOptions { - this._inGrid = true; - const options = { - grid_columns: 2, - grid_rows: 1, - }; - if (!this._config) return options; - const appearance = computeAppearance(this._config); - if (appearance.layout === "vertical") { - options.grid_rows += 1; - } - if (appearance.layout === "horizontal") { - options.grid_columns = 4; - } - if (appearance?.layout !== "horizontal" && this.hasControls) { - options.grid_rows += 1; - } - return options; + if ( + appearance?.layout !== "horizontal" && + this.hasControls && + !( + "collapsible_controls" in this._config && + this._config?.collapsible_controls + ) + ) { + height += 1; } - - protected renderPicture(picture: string): TemplateResult { - return html` - - `; + return height; + } + + public getLayoutOptions(): LovelaceLayoutOptions { + this._inGrid = true; + const options = { + grid_columns: 2, + grid_rows: 1, + }; + if (!this._config) return options; + const appearance = computeAppearance(this._config); + if (appearance.layout === "vertical") { + options.grid_rows += 1; } - - protected renderNotFound(config: BaseConfig): TemplateResult { - const appearance = computeAppearance(config); - const rtl = computeRTL(this.hass); - - const customLocalize = setupCustomlocalize(this.hass); - - return html` - - - - - - - - - - - - `; + if (appearance.layout === "horizontal") { + options.grid_columns = 4; } - - protected renderIcon(stateObj: HassEntity, icon?: string): TemplateResult { - const active = isActive(stateObj); - return html` - - - `; + if (appearance?.layout !== "horizontal" && this.hasControls) { + options.grid_rows += 1; } - - protected renderBadge(stateObj: HassEntity) { - const unavailable = !isAvailable(stateObj); - return unavailable - ? html` - - ` - : nothing; - } - - protected renderStateInfo( - stateObj: HassEntity, - appearance: Appearance, - name: string, - state?: string - ): TemplateResult | null { - const defaultState = this.hass.formatEntityState - ? this.hass.formatEntityState(stateObj) - : computeStateDisplay( - this.hass.localize, - stateObj, - this.hass.locale, - this.hass.config, - this.hass.entities - ); - const displayState = state ?? defaultState; - - const primary = computeInfoDisplay( - appearance.primary_info, - name, - displayState, - stateObj, - this.hass - ); - - const secondary = computeInfoDisplay( - appearance.secondary_info, - name, - displayState, - stateObj, - this.hass - ); - - return html` + return options; + } + + protected renderPicture(picture: string): TemplateResult { + return html` + + `; + } + + protected renderNotFound(config: BaseConfig): TemplateResult { + const appearance = computeAppearance(config); + const rtl = computeRTL(this.hass); + + const customLocalize = setupCustomlocalize(this.hass); + + return html` + + + + + + + - `; - } + + + + `; + } + + protected renderIcon(stateObj: HassEntity, icon?: string): TemplateResult { + const active = isActive(stateObj); + return html` + + + `; + } + + protected renderBadge(stateObj: HassEntity) { + const unavailable = !isAvailable(stateObj); + return unavailable + ? html` + + ` + : nothing; + } + + protected renderStateInfo( + stateObj: HassEntity, + appearance: Appearance, + name: string, + state?: string + ): TemplateResult | null { + const defaultState = this.hass.formatEntityState + ? this.hass.formatEntityState(stateObj) + : computeStateDisplay( + this.hass.localize, + stateObj, + this.hass.locale, + this.hass.config, + this.hass.entities + ); + const displayState = state ?? defaultState; + + const primary = computeInfoDisplay( + appearance.primary_info, + name, + displayState, + stateObj, + this.hass + ); + + const secondary = computeInfoDisplay( + appearance.secondary_info, + name, + displayState, + stateObj, + this.hass + ); + + return html` + + `; + } } diff --git a/src/utils/base-element.ts b/src/utils/base-element.ts index c0c9be64e..2b469a142 100644 --- a/src/utils/base-element.ts +++ b/src/utils/base-element.ts @@ -12,38 +12,38 @@ import { defaultColorCss, defaultDarkColorCss } from "./colors"; import { themeColorCss, themeVariables } from "./theme"; export function computeDarkMode(hass?: HomeAssistant): boolean { - if (!hass) return false; - return (hass.themes as any).darkMode as boolean; + if (!hass) return false; + return (hass.themes as any).darkMode as boolean; } export class MushroomBaseElement extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - protected updated(changedProps: PropertyValues): void { - super.updated(changedProps); - if (changedProps.has("hass") && this.hass) { - const currentDarkMode = computeDarkMode(changedProps.get("hass")); - const newDarkMode = computeDarkMode(this.hass); - if (currentDarkMode !== newDarkMode) { - this.toggleAttribute("dark-mode", newDarkMode); - } - } + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if (changedProps.has("hass") && this.hass) { + const currentDarkMode = computeDarkMode(changedProps.get("hass")); + const newDarkMode = computeDarkMode(this.hass); + if (currentDarkMode !== newDarkMode) { + this.toggleAttribute("dark-mode", newDarkMode); + } } + } - static get styles(): CSSResultGroup { - return [ - animations, - css` - :host { - ${defaultColorCss} - } - :host([dark-mode]) { - ${defaultDarkColorCss} - } - :host { - ${themeColorCss} - ${themeVariables} - } - `, - ]; - } + static get styles(): CSSResultGroup { + return [ + animations, + css` + :host { + ${defaultColorCss} + } + :host([dark-mode]) { + ${defaultDarkColorCss} + } + :host { + ${themeColorCss} + ${themeVariables} + } + `, + ]; + } } diff --git a/src/utils/card-styles.ts b/src/utils/card-styles.ts index 6b54841a6..8a472870d 100644 --- a/src/utils/card-styles.ts +++ b/src/utils/card-styles.ts @@ -1,53 +1,53 @@ import { css } from "lit"; export const cardStyle = css` - ha-card { - box-sizing: border-box; - padding: var(--spacing); - display: flex; - flex-direction: column; - justify-content: var(--layout-align); - height: auto; - } - ha-card.fill-container { - height: 100%; - } - :host([layout="grid"]) ha-card, - :host([in-grid]) ha-card { - height: 100%; - } - :host([layout="grid"]) ha-card mushroom-card, - :host([in-grid]) ha-card mushroom-card { - height: 100%; - } - .actions { - display: flex; - flex-direction: row; - align-items: flex-start; - justify-content: flex-start; - overflow-x: auto; - overflow-y: hidden; - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE 10+ */ - } - .actions::-webkit-scrollbar { - background: transparent; /* Chrome/Safari/Webkit */ - height: 0px; - } - .actions *:not(:last-child) { - margin-right: var(--spacing); - } - .actions[rtl] *:not(:last-child) { - margin-right: initial; - margin-left: var(--spacing); - } - .unavailable { - --main-color: rgb(var(--rgb-warning)); - } - .not-found { - --main-color: rgb(var(--rgb-danger)); - } - mushroom-state-item[disabled] { - cursor: initial; - } + ha-card { + box-sizing: border-box; + padding: var(--spacing); + display: flex; + flex-direction: column; + justify-content: var(--layout-align); + height: auto; + } + ha-card.fill-container { + height: 100%; + } + :host([layout="grid"]) ha-card, + :host([in-grid]) ha-card { + height: 100%; + } + :host([layout="grid"]) ha-card mushroom-card, + :host([in-grid]) ha-card mushroom-card { + height: 100%; + } + .actions { + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE 10+ */ + } + .actions::-webkit-scrollbar { + background: transparent; /* Chrome/Safari/Webkit */ + height: 0px; + } + .actions *:not(:last-child) { + margin-right: var(--spacing); + } + .actions[rtl] *:not(:last-child) { + margin-right: initial; + margin-left: var(--spacing); + } + .unavailable { + --main-color: rgb(var(--rgb-warning)); + } + .not-found { + --main-color: rgb(var(--rgb-danger)); + } + mushroom-state-item[disabled] { + cursor: initial; + } `; diff --git a/src/utils/colors.ts b/src/utils/colors.ts index d291af323..9fc5e5e07 100644 --- a/src/utils/colors.ts +++ b/src/utils/colors.ts @@ -2,88 +2,88 @@ import { css } from "lit"; import * as Color from "color"; export const COLORS = [ - "primary", - "accent", - "red", - "pink", - "purple", - "deep-purple", - "indigo", - "blue", - "light-blue", - "cyan", - "teal", - "green", - "light-green", - "lime", - "yellow", - "amber", - "orange", - "deep-orange", - "brown", - "light-grey", - "grey", - "dark-grey", - "blue-grey", - "black", - "white", - "disabled", + "primary", + "accent", + "red", + "pink", + "purple", + "deep-purple", + "indigo", + "blue", + "light-blue", + "cyan", + "teal", + "green", + "light-green", + "lime", + "yellow", + "amber", + "orange", + "deep-orange", + "brown", + "light-grey", + "grey", + "dark-grey", + "blue-grey", + "black", + "white", + "disabled", ]; export function computeRgbColor(color: string): string { - if (color === "primary" || color === "accent") { - return `var(--rgb-${color}-color)`; + if (color === "primary" || color === "accent") { + return `var(--rgb-${color}-color)`; + } + if (COLORS.includes(color)) { + return `var(--rgb-${color})`; + } else if (color.startsWith("#")) { + try { + return Color.rgb(color).rgb().array().join(", "); + } catch (err) { + return ""; } - if (COLORS.includes(color)) { - return `var(--rgb-${color})`; - } else if (color.startsWith("#")) { - try { - return Color.rgb(color).rgb().array().join(", "); - } catch (err) { - return ""; - } - } - return color; + } + return color; } export function computeColorName(color: string): string { - return color - .split("-") - .map((s) => capitalizeFirstLetter(s)) - .join(" "); + return color + .split("-") + .map((s) => capitalizeFirstLetter(s)) + .join(" "); } function capitalizeFirstLetter(string) { - return string.charAt(0).toUpperCase() + string.slice(1); + return string.charAt(0).toUpperCase() + string.slice(1); } export const defaultColorCss = css` - --default-red: 244, 67, 54; - --default-pink: 233, 30, 99; - --default-purple: 106, 107, 201; - --default-deep-purple: 111, 66, 193; - --default-indigo: 63, 81, 181; - --default-blue: 33, 150, 243; - --default-light-blue: 3, 169, 244; - --default-cyan: 0, 188, 212; - --default-teal: 0, 150, 136; - --default-green: 76, 175, 80; - --default-light-green: 139, 195, 74; - --default-lime: 205, 220, 57; - --default-yellow: 255, 235, 59; - --default-amber: 255, 193, 7; - --default-orange: 255, 152, 0; - --default-deep-orange: 255, 111, 0; - --default-brown: 121, 85, 72; - --default-light-grey: 189, 189, 189; - --default-grey: 158, 158, 158; - --default-dark-grey: 96, 96, 96; - --default-blue-grey: 96, 125, 139; - --default-black: 0, 0, 0; - --default-white: 255, 255, 255; - --default-disabled: 189, 189, 189; + --default-red: 244, 67, 54; + --default-pink: 233, 30, 99; + --default-purple: 106, 107, 201; + --default-deep-purple: 111, 66, 193; + --default-indigo: 63, 81, 181; + --default-blue: 33, 150, 243; + --default-light-blue: 3, 169, 244; + --default-cyan: 0, 188, 212; + --default-teal: 0, 150, 136; + --default-green: 76, 175, 80; + --default-light-green: 139, 195, 74; + --default-lime: 205, 220, 57; + --default-yellow: 255, 235, 59; + --default-amber: 255, 193, 7; + --default-orange: 255, 152, 0; + --default-deep-orange: 255, 111, 0; + --default-brown: 121, 85, 72; + --default-light-grey: 189, 189, 189; + --default-grey: 158, 158, 158; + --default-dark-grey: 96, 96, 96; + --default-blue-grey: 96, 125, 139; + --default-black: 0, 0, 0; + --default-white: 255, 255, 255; + --default-disabled: 189, 189, 189; `; export const defaultDarkColorCss = css` - --default-disabled: 111, 111, 111; + --default-disabled: 111, 111, 111; `; diff --git a/src/utils/custom-cards.ts b/src/utils/custom-cards.ts index 5a667f72e..78890ff0c 100644 --- a/src/utils/custom-cards.ts +++ b/src/utils/custom-cards.ts @@ -1,20 +1,20 @@ import { repository } from "../../package.json"; interface RegisterCardParams { - type: string; - name: string; - description: string; + type: string; + name: string; + description: string; } export function registerCustomCard(params: RegisterCardParams) { - const windowWithCards = window as unknown as Window & { - customCards: unknown[]; - }; - windowWithCards.customCards = windowWithCards.customCards || []; + const windowWithCards = window as unknown as Window & { + customCards: unknown[]; + }; + windowWithCards.customCards = windowWithCards.customCards || []; - const cardPage = params.type.replace("-card", "").replace("mushroom-", ""); - windowWithCards.customCards.push({ - ...params, - preview: true, - documentationURL: `${repository.url}/blob/main/docs/cards/${cardPage}.md`, - }); + const cardPage = params.type.replace("-card", "").replace("mushroom-", ""); + windowWithCards.customCards.push({ + ...params, + preview: true, + documentationURL: `${repository.url}/blob/main/docs/cards/${cardPage}.md`, + }); } diff --git a/src/utils/entity-styles.ts b/src/utils/entity-styles.ts index 44948b62f..4b4dcc39f 100644 --- a/src/utils/entity-styles.ts +++ b/src/utils/entity-styles.ts @@ -1,7 +1,7 @@ import { css, unsafeCSS } from "lit"; const strAnimations = { - pulse: `@keyframes pulse { + pulse: `@keyframes pulse { 0% { opacity: 1; } @@ -12,7 +12,7 @@ const strAnimations = { opacity: 1; } }`, - spin: `@keyframes spin { + spin: `@keyframes spin { from { transform: rotate(0deg); } @@ -20,7 +20,7 @@ const strAnimations = { transform: rotate(360deg); } }`, - cleaning: `@keyframes cleaning { + cleaning: `@keyframes cleaning { 0% { transform: rotate(0) translate(0); } @@ -69,7 +69,7 @@ const strAnimations = { transform: rotate(0deg); } }`, - returning: `@keyframes returning { + returning: `@keyframes returning { 0% { transform: rotate(0); } @@ -89,19 +89,19 @@ const strAnimations = { }; export const animation = { - pulse: css` - ${unsafeCSS(strAnimations.pulse)} - `, - spin: css` - ${unsafeCSS(strAnimations.spin)} - `, - cleaning: css` - ${unsafeCSS(strAnimations.cleaning)} - `, - returning: css` - ${unsafeCSS(strAnimations.returning)} - `, + pulse: css` + ${unsafeCSS(strAnimations.pulse)} + `, + spin: css` + ${unsafeCSS(strAnimations.spin)} + `, + cleaning: css` + ${unsafeCSS(strAnimations.cleaning)} + `, + returning: css` + ${unsafeCSS(strAnimations.returning)} + `, }; export const animations = css` - ${unsafeCSS(Object.values(strAnimations).join("\n"))} + ${unsafeCSS(Object.values(strAnimations).join("\n"))} `; diff --git a/src/utils/form/custom/ha-selector-mushroom-alignment.ts b/src/utils/form/custom/ha-selector-mushroom-alignment.ts index 9d56275bd..42a755241 100644 --- a/src/utils/form/custom/ha-selector-mushroom-alignment.ts +++ b/src/utils/form/custom/ha-selector-mushroom-alignment.ts @@ -4,31 +4,31 @@ import { fireEvent, HomeAssistant } from "../../../ha"; import "../../../shared/editor/alignment-picker"; export type MushAlignementSelector = { - mush_alignment: {}; + mush_alignment: {}; }; @customElement("ha-selector-mush_alignment") export class HaMushAlignmentSelector extends LitElement { - @property() public hass!: HomeAssistant; + @property() public hass!: HomeAssistant; - @property() public selector!: MushAlignementSelector; + @property() public selector!: MushAlignementSelector; - @property() public value?: string; + @property() public value?: string; - @property() public label?: string; + @property() public label?: string; - protected render() { - return html` - - `; - } + protected render() { + return html` + + `; + } - private _valueChanged(ev: CustomEvent) { - fireEvent(this, "value-changed", { value: ev.detail.value || undefined }); - } + private _valueChanged(ev: CustomEvent) { + fireEvent(this, "value-changed", { value: ev.detail.value || undefined }); + } } diff --git a/src/utils/form/custom/ha-selector-mushroom-color.ts b/src/utils/form/custom/ha-selector-mushroom-color.ts index b7f3bd0dd..23edf097f 100644 --- a/src/utils/form/custom/ha-selector-mushroom-color.ts +++ b/src/utils/form/custom/ha-selector-mushroom-color.ts @@ -4,31 +4,31 @@ import { fireEvent, HomeAssistant } from "../../../ha"; import "../../../shared/editor/color-picker"; export type MushColorSelector = { - mush_color: {}; + mush_color: {}; }; @customElement("ha-selector-mush_color") export class HaMushColorSelector extends LitElement { - @property() public hass!: HomeAssistant; + @property() public hass!: HomeAssistant; - @property() public selector!: MushColorSelector; + @property() public selector!: MushColorSelector; - @property() public value?: string; + @property() public value?: string; - @property() public label?: string; + @property() public label?: string; - protected render() { - return html` - - `; - } + protected render() { + return html` + + `; + } - private _valueChanged(ev: CustomEvent) { - fireEvent(this, "value-changed", { value: ev.detail.value || undefined }); - } + private _valueChanged(ev: CustomEvent) { + fireEvent(this, "value-changed", { value: ev.detail.value || undefined }); + } } diff --git a/src/utils/form/custom/ha-selector-mushroom-icon-type.ts b/src/utils/form/custom/ha-selector-mushroom-icon-type.ts index 5c0951673..cacbc6968 100644 --- a/src/utils/form/custom/ha-selector-mushroom-icon-type.ts +++ b/src/utils/form/custom/ha-selector-mushroom-icon-type.ts @@ -4,31 +4,31 @@ import { customElement, property } from "lit/decorators.js"; import "../../../shared/editor/icon-type-picker"; export type MushIconTypeSelector = { - mush_icon_type: {}; + mush_icon_type: {}; }; @customElement("ha-selector-mush_icon_type") export class HaMushIconTypeSelector extends LitElement { - @property() public hass!: HomeAssistant; + @property() public hass!: HomeAssistant; - @property() public selector!: MushIconTypeSelector; + @property() public selector!: MushIconTypeSelector; - @property() public value?: string; + @property() public value?: string; - @property() public label?: string; + @property() public label?: string; - protected render() { - return html` - - `; - } + protected render() { + return html` + + `; + } - private _valueChanged(ev: CustomEvent) { - fireEvent(this, "value-changed", { value: ev.detail.value || undefined }); - } + private _valueChanged(ev: CustomEvent) { + fireEvent(this, "value-changed", { value: ev.detail.value || undefined }); + } } diff --git a/src/utils/form/custom/ha-selector-mushroom-info.ts b/src/utils/form/custom/ha-selector-mushroom-info.ts index 35ee80e1b..55fd91a8c 100644 --- a/src/utils/form/custom/ha-selector-mushroom-info.ts +++ b/src/utils/form/custom/ha-selector-mushroom-info.ts @@ -5,34 +5,34 @@ import "../../../shared/editor/info-picker"; import { Info } from "../../info"; export type MushInfoSelector = { - mush_info: { - infos?: Info[]; - }; + mush_info: { + infos?: Info[]; + }; }; @customElement("ha-selector-mush_info") export class HaMushInfoSelector extends LitElement { - @property() public hass!: HomeAssistant; + @property() public hass!: HomeAssistant; - @property() public selector!: MushInfoSelector; + @property() public selector!: MushInfoSelector; - @property() public value?: string; + @property() public value?: string; - @property() public label?: string; + @property() public label?: string; - protected render() { - return html` - - `; - } + protected render() { + return html` + + `; + } - private _valueChanged(ev: CustomEvent) { - fireEvent(this, "value-changed", { value: ev.detail.value || undefined }); - } + private _valueChanged(ev: CustomEvent) { + fireEvent(this, "value-changed", { value: ev.detail.value || undefined }); + } } diff --git a/src/utils/form/custom/ha-selector-mushroom-layout.ts b/src/utils/form/custom/ha-selector-mushroom-layout.ts index 4de86bc1b..fd0487aed 100644 --- a/src/utils/form/custom/ha-selector-mushroom-layout.ts +++ b/src/utils/form/custom/ha-selector-mushroom-layout.ts @@ -4,31 +4,31 @@ import { fireEvent, HomeAssistant } from "../../../ha"; import "../../../shared/editor/layout-picker"; export type MushLayoutSelector = { - mush_layout: {}; + mush_layout: {}; }; @customElement("ha-selector-mush_layout") export class HaMushLayoutSelector extends LitElement { - @property() public hass!: HomeAssistant; + @property() public hass!: HomeAssistant; - @property() public selector!: MushLayoutSelector; + @property() public selector!: MushLayoutSelector; - @property() public value?: string; + @property() public value?: string; - @property() public label?: string; + @property() public label?: string; - protected render() { - return html` - - `; - } + protected render() { + return html` + + `; + } - private _valueChanged(ev: CustomEvent) { - fireEvent(this, "value-changed", { value: ev.detail.value || undefined }); - } + private _valueChanged(ev: CustomEvent) { + fireEvent(this, "value-changed", { value: ev.detail.value || undefined }); + } } diff --git a/src/utils/form/generic-fields.ts b/src/utils/form/generic-fields.ts index 51195dbcf..9367bcdd0 100644 --- a/src/utils/form/generic-fields.ts +++ b/src/utils/form/generic-fields.ts @@ -1,12 +1,12 @@ export const GENERIC_LABELS = [ - "icon_color", - "layout", - "fill_container", - "primary_info", - "secondary_info", - "icon_type", - "content_info", - "use_entity_picture", - "collapsible_controls", - "icon_animation", + "icon_color", + "layout", + "fill_container", + "primary_info", + "secondary_info", + "icon_type", + "content_info", + "use_entity_picture", + "collapsible_controls", + "icon_animation", ]; diff --git a/src/utils/form/ha-form.ts b/src/utils/form/ha-form.ts index 0e99a34ac..1f263bb5c 100644 --- a/src/utils/form/ha-form.ts +++ b/src/utils/form/ha-form.ts @@ -2,100 +2,100 @@ import type { LitElement } from "lit"; import { Selector } from "./ha-selector"; interface HaDurationData { - hours?: number; - minutes?: number; - seconds?: number; - milliseconds?: number; + hours?: number; + minutes?: number; + seconds?: number; + milliseconds?: number; } export type HaFormSchema = - | HaFormConstantSchema - | HaFormStringSchema - | HaFormIntegerSchema - | HaFormFloatSchema - | HaFormBooleanSchema - | HaFormSelectSchema - | HaFormMultiSelectSchema - | HaFormTimeSchema - | HaFormSelector - | HaFormGridSchema; + | HaFormConstantSchema + | HaFormStringSchema + | HaFormIntegerSchema + | HaFormFloatSchema + | HaFormBooleanSchema + | HaFormSelectSchema + | HaFormMultiSelectSchema + | HaFormTimeSchema + | HaFormSelector + | HaFormGridSchema; export interface HaFormBaseSchema { - name: string; - // This value is applied if no data is submitted for this field - default?: HaFormData; - required?: boolean; - description?: { - suffix?: string; - // This value will be set initially when form is loaded - suggested_value?: HaFormData; - }; - context?: Record; + name: string; + // This value is applied if no data is submitted for this field + default?: HaFormData; + required?: boolean; + description?: { + suffix?: string; + // This value will be set initially when form is loaded + suggested_value?: HaFormData; + }; + context?: Record; } export interface HaFormGridSchema extends HaFormBaseSchema { - type: "grid"; - name: ""; - column_min_width?: string; - schema: HaFormSchema[]; + type: "grid"; + name: ""; + column_min_width?: string; + schema: HaFormSchema[]; } export interface HaFormSelector extends HaFormBaseSchema { - type?: never; - selector: Selector; + type?: never; + selector: Selector; } export interface HaFormConstantSchema extends HaFormBaseSchema { - type: "constant"; - value?: string; + type: "constant"; + value?: string; } export interface HaFormIntegerSchema extends HaFormBaseSchema { - type: "integer"; - default?: HaFormIntegerData; - valueMin?: number; - valueMax?: number; + type: "integer"; + default?: HaFormIntegerData; + valueMin?: number; + valueMax?: number; } export interface HaFormSelectSchema extends HaFormBaseSchema { - type: "select"; - options: Array<[string, string]>; + type: "select"; + options: Array<[string, string]>; } export interface HaFormMultiSelectSchema extends HaFormBaseSchema { - type: "multi_select"; - options: Record | string[] | Array<[string, string]>; + type: "multi_select"; + options: Record | string[] | Array<[string, string]>; } export interface HaFormFloatSchema extends HaFormBaseSchema { - type: "float"; + type: "float"; } export interface HaFormStringSchema extends HaFormBaseSchema { - type: "string"; - format?: string; + type: "string"; + format?: string; } export interface HaFormBooleanSchema extends HaFormBaseSchema { - type: "boolean"; + type: "boolean"; } export interface HaFormTimeSchema extends HaFormBaseSchema { - type: "positive_time_period_dict"; + type: "positive_time_period_dict"; } export interface HaFormDataContainer { - [key: string]: HaFormData; + [key: string]: HaFormData; } export type HaFormData = - | HaFormStringData - | HaFormIntegerData - | HaFormFloatData - | HaFormBooleanData - | HaFormSelectData - | HaFormMultiSelectData - | HaFormTimeData; + | HaFormStringData + | HaFormIntegerData + | HaFormFloatData + | HaFormBooleanData + | HaFormSelectData + | HaFormMultiSelectData + | HaFormTimeData; export type HaFormStringData = string; export type HaFormIntegerData = number; @@ -106,7 +106,7 @@ export type HaFormMultiSelectData = string[]; export type HaFormTimeData = HaDurationData; export interface HaFormElement extends LitElement { - schema: HaFormSchema | HaFormSchema[]; - data?: HaFormDataContainer | HaFormData; - label?: string; + schema: HaFormSchema | HaFormSchema[]; + data?: HaFormDataContainer | HaFormData; + label?: string; } diff --git a/src/utils/form/ha-selector.ts b/src/utils/form/ha-selector.ts index c9836b70c..855b6da34 100644 --- a/src/utils/form/ha-selector.ts +++ b/src/utils/form/ha-selector.ts @@ -6,248 +6,248 @@ import { MushInfoSelector } from "./custom/ha-selector-mushroom-info"; import { MushLayoutSelector } from "./custom/ha-selector-mushroom-layout"; type MushSelector = - | MushColorSelector - | MushLayoutSelector - | MushInfoSelector - | MushIconTypeSelector - | MushAlignementSelector; + | MushColorSelector + | MushLayoutSelector + | MushInfoSelector + | MushIconTypeSelector + | MushAlignementSelector; export type Selector = - | ActionSelector - | AddonSelector - | AreaSelector - | AttributeSelector - | BooleanSelector - | ColorRGBSelector - | ColorTempSelector - | DateSelector - | DateTimeSelector - | DeviceSelector - | DurationSelector - | EntitySelector - | IconSelector - | LocationSelector - | MediaSelector - | NumberSelector - | ObjectSelector - | SelectSelector - | StringSelector - | TargetSelector - | TemplateSelector - | ThemeSelector - | TimeSelector - | UiActionSelector - | MushSelector; + | ActionSelector + | AddonSelector + | AreaSelector + | AttributeSelector + | BooleanSelector + | ColorRGBSelector + | ColorTempSelector + | DateSelector + | DateTimeSelector + | DeviceSelector + | DurationSelector + | EntitySelector + | IconSelector + | LocationSelector + | MediaSelector + | NumberSelector + | ObjectSelector + | SelectSelector + | StringSelector + | TargetSelector + | TemplateSelector + | ThemeSelector + | TimeSelector + | UiActionSelector + | MushSelector; export interface ActionSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - action: {}; + // eslint-disable-next-line @typescript-eslint/ban-types + action: {}; } export interface AddonSelector { - addon: { - name?: string; - slug?: string; - }; + addon: { + name?: string; + slug?: string; + }; } export interface AreaSelector { - area: { - entity?: { - integration?: EntitySelector["entity"]["integration"]; - domain?: EntitySelector["entity"]["domain"]; - device_class?: EntitySelector["entity"]["device_class"]; - }; - device?: { - integration?: DeviceSelector["device"]["integration"]; - manufacturer?: DeviceSelector["device"]["manufacturer"]; - model?: DeviceSelector["device"]["model"]; - }; - multiple?: boolean; + area: { + entity?: { + integration?: EntitySelector["entity"]["integration"]; + domain?: EntitySelector["entity"]["domain"]; + device_class?: EntitySelector["entity"]["device_class"]; + }; + device?: { + integration?: DeviceSelector["device"]["integration"]; + manufacturer?: DeviceSelector["device"]["manufacturer"]; + model?: DeviceSelector["device"]["model"]; }; + multiple?: boolean; + }; } export interface AttributeSelector { - attribute: { - entity_id?: string; - }; + attribute: { + entity_id?: string; + }; } export interface BooleanSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - boolean: {}; + // eslint-disable-next-line @typescript-eslint/ban-types + boolean: {}; } export interface ColorRGBSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - color_rgb: {}; + // eslint-disable-next-line @typescript-eslint/ban-types + color_rgb: {}; } export interface ColorTempSelector { - color_temp: { - min_mireds?: number; - max_mireds?: number; - }; + color_temp: { + min_mireds?: number; + max_mireds?: number; + }; } export interface DateSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - date: {}; + // eslint-disable-next-line @typescript-eslint/ban-types + date: {}; } export interface DateTimeSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - datetime: {}; + // eslint-disable-next-line @typescript-eslint/ban-types + datetime: {}; } export interface DeviceSelector { - device: { - integration?: string; - manufacturer?: string; - model?: string; - entity?: { - domain?: EntitySelector["entity"]["domain"]; - device_class?: EntitySelector["entity"]["device_class"]; - }; - multiple?: boolean; + device: { + integration?: string; + manufacturer?: string; + model?: string; + entity?: { + domain?: EntitySelector["entity"]["domain"]; + device_class?: EntitySelector["entity"]["device_class"]; }; + multiple?: boolean; + }; } export interface DurationSelector { - duration: { - enable_day?: boolean; - }; + duration: { + enable_day?: boolean; + }; } export interface EntitySelector { - entity: { - integration?: string; - domain?: string | string[]; - device_class?: string; - multiple?: boolean; - include_entities?: string[]; - exclude_entities?: string[]; - }; + entity: { + integration?: string; + domain?: string | string[]; + device_class?: string; + multiple?: boolean; + include_entities?: string[]; + exclude_entities?: string[]; + }; } export interface IconSelector { - icon: { - placeholder?: string; - fallbackPath?: string; - }; + icon: { + placeholder?: string; + fallbackPath?: string; + }; } export interface LocationSelector { - location: { radius?: boolean; icon?: string }; + location: { radius?: boolean; icon?: string }; } export interface LocationSelectorValue { - latitude: number; - longitude: number; - radius?: number; + latitude: number; + longitude: number; + radius?: number; } export interface MediaSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - media: {}; + // eslint-disable-next-line @typescript-eslint/ban-types + media: {}; } export interface MediaSelectorValue { - entity_id?: string; - media_content_id?: string; - media_content_type?: string; - metadata?: { - title?: string; - thumbnail?: string | null; - media_class?: string; - children_media_class?: string | null; - navigateIds?: { media_content_type: string; media_content_id: string }[]; - }; + entity_id?: string; + media_content_id?: string; + media_content_type?: string; + metadata?: { + title?: string; + thumbnail?: string | null; + media_class?: string; + children_media_class?: string | null; + navigateIds?: { media_content_type: string; media_content_id: string }[]; + }; } export interface NumberSelector { - number: { - min?: number; - max?: number; - step?: number; - mode?: "box" | "slider"; - unit_of_measurement?: string; - }; + number: { + min?: number; + max?: number; + step?: number; + mode?: "box" | "slider"; + unit_of_measurement?: string; + }; } export interface ObjectSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - object: {}; + // eslint-disable-next-line @typescript-eslint/ban-types + object: {}; } export interface SelectOption { - value: string; - label: string; + value: string; + label: string; } export interface SelectSelector { - select: { - multiple?: boolean; - custom_value?: boolean; - mode?: "list" | "dropdown"; - options: string[] | SelectOption[]; - }; + select: { + multiple?: boolean; + custom_value?: boolean; + mode?: "list" | "dropdown"; + options: string[] | SelectOption[]; + }; } export interface StringSelector { - text: { - multiline?: boolean; - type?: - | "number" - | "text" - | "search" - | "tel" - | "url" - | "email" - | "password" - | "date" - | "month" - | "week" - | "time" - | "datetime-local" - | "color"; - suffix?: string; - }; + text: { + multiline?: boolean; + type?: + | "number" + | "text" + | "search" + | "tel" + | "url" + | "email" + | "password" + | "date" + | "month" + | "week" + | "time" + | "datetime-local" + | "color"; + suffix?: string; + }; } export interface TargetSelector { - target: { - entity?: { - integration?: EntitySelector["entity"]["integration"]; - domain?: EntitySelector["entity"]["domain"]; - device_class?: EntitySelector["entity"]["device_class"]; - }; - device?: { - integration?: DeviceSelector["device"]["integration"]; - manufacturer?: DeviceSelector["device"]["manufacturer"]; - model?: DeviceSelector["device"]["model"]; - }; + target: { + entity?: { + integration?: EntitySelector["entity"]["integration"]; + domain?: EntitySelector["entity"]["domain"]; + device_class?: EntitySelector["entity"]["device_class"]; + }; + device?: { + integration?: DeviceSelector["device"]["integration"]; + manufacturer?: DeviceSelector["device"]["manufacturer"]; + model?: DeviceSelector["device"]["model"]; }; + }; } export interface TemplateSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - template: {}; + // eslint-disable-next-line @typescript-eslint/ban-types + template: {}; } export interface ThemeSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - theme: {}; + // eslint-disable-next-line @typescript-eslint/ban-types + theme: {}; } export interface TimeSelector { - // eslint-disable-next-line @typescript-eslint/ban-types - time: {}; + // eslint-disable-next-line @typescript-eslint/ban-types + time: {}; } export type UiAction = Exclude; export interface UiActionSelector { - "ui-action": { - actions?: UiAction[]; - } | null; + "ui-action": { + actions?: UiAction[]; + } | null; } diff --git a/src/utils/icons/cover-icon.ts b/src/utils/icons/cover-icon.ts index b5e6bc6f7..43655d42c 100644 --- a/src/utils/icons/cover-icon.ts +++ b/src/utils/icons/cover-icon.ts @@ -1,25 +1,25 @@ import { HassEntity } from "home-assistant-js-websocket"; export const computeOpenIcon = (stateObj: HassEntity): string => { - switch (stateObj.attributes.device_class) { - case "awning": - case "curtain": - case "door": - case "gate": - return "mdi:arrow-expand-horizontal"; - default: - return "mdi:arrow-up"; - } + switch (stateObj.attributes.device_class) { + case "awning": + case "curtain": + case "door": + case "gate": + return "mdi:arrow-expand-horizontal"; + default: + return "mdi:arrow-up"; + } }; export const computeCloseIcon = (stateObj: HassEntity): string => { - switch (stateObj.attributes.device_class) { - case "awning": - case "curtain": - case "door": - case "gate": - return "mdi:arrow-collapse-horizontal"; - default: - return "mdi:arrow-down"; - } + switch (stateObj.attributes.device_class) { + case "awning": + case "curtain": + case "door": + case "gate": + return "mdi:arrow-collapse-horizontal"; + default: + return "mdi:arrow-down"; + } }; diff --git a/src/utils/icons/weather-icon.ts b/src/utils/icons/weather-icon.ts index a2c6c141c..fd6206b8f 100644 --- a/src/utils/icons/weather-icon.ts +++ b/src/utils/icons/weather-icon.ts @@ -1,29 +1,29 @@ import { getWeatherStateSVG } from "../weather"; export const weatherSVGs = new Set([ - "clear-night", - "cloudy", - "fog", - "lightning", - "lightning-rainy", - "partlycloudy", - "pouring", - "rainy", - "hail", - "snowy", - "snowy-rainy", - "sunny", - "windy", - "windy-variant", + "clear-night", + "cloudy", + "fog", + "lightning", + "lightning-rainy", + "partlycloudy", + "pouring", + "rainy", + "hail", + "snowy", + "snowy-rainy", + "sunny", + "windy", + "windy-variant", ]); export const getWeatherSvgIcon = (icon?: string) => { - if (!icon || !icon.startsWith("weather-")) { - return undefined; - } - const name = icon.replace("weather-", ""); - if (!weatherSVGs.has(name)) { - return undefined; - } - return getWeatherStateSVG(name, true); + if (!icon || !icon.startsWith("weather-")) { + return undefined; + } + const name = icon.replace("weather-", ""); + if (!weatherSVGs.has(name)) { + return undefined; + } + return getWeatherStateSVG(name, true); }; diff --git a/src/utils/info.ts b/src/utils/info.ts index f494f411c..f0b884985 100644 --- a/src/utils/info.ts +++ b/src/utils/info.ts @@ -4,61 +4,67 @@ import { getEntityPicture, HomeAssistant, isAvailable, isUnknown } from "../ha"; const TIMESTAMP_STATE_DOMAINS = ["button", "input_button", "scene"]; -export const INFOS = ["name", "state", "last-changed", "last-updated", "none"] as const; +export const INFOS = [ + "name", + "state", + "last-changed", + "last-updated", + "none", +] as const; export type Info = (typeof INFOS)[number]; export const ICON_TYPES = ["icon", "entity-picture", "none"] as const; export type IconType = (typeof ICON_TYPES)[number]; export function computeInfoDisplay( - info: Info, - name: string, - state: string, - stateObj: HassEntity, - hass: HomeAssistant + info: Info, + name: string, + state: string, + stateObj: HassEntity, + hass: HomeAssistant ) { - switch (info) { - case "name": - return name; - case "state": - const domain = stateObj.entity_id.split(".")[0]; - if ( - (stateObj.attributes.device_class === "timestamp" || - TIMESTAMP_STATE_DOMAINS.includes(domain)) && - isAvailable(stateObj) && - !isUnknown(stateObj) - ) { - return html` - - `; - } else { - return state; - } - case "last-changed": - return html` - - `; - case "last-updated": - return html` - - `; - case "none": - return undefined; - } + switch (info) { + case "name": + return name; + case "state": + const domain = stateObj.entity_id.split(".")[0]; + if ( + (stateObj.attributes.device_class === "timestamp" || + TIMESTAMP_STATE_DOMAINS.includes(domain)) && + isAvailable(stateObj) && + !isUnknown(stateObj) + ) { + return html` + + `; + } else { + return state; + } + case "last-changed": + return html` + + `; + case "last-updated": + return html` + + `; + case "none": + return undefined; + } } export function computeEntityPicture(stateObj: HassEntity, iconType: IconType) { - return iconType === "entity-picture" ? getEntityPicture(stateObj) : undefined; + return iconType === "entity-picture" ? getEntityPicture(stateObj) : undefined; } diff --git a/src/utils/layout.ts b/src/utils/layout.ts index dd1190373..d8d8b98ee 100644 --- a/src/utils/layout.ts +++ b/src/utils/layout.ts @@ -2,4 +2,8 @@ import { literal, union } from "superstruct"; export type Layout = "vertical" | "horizontal" | "default"; -export const layoutStruct = union([literal("horizontal"), literal("vertical"), literal("default")]); +export const layoutStruct = union([ + literal("horizontal"), + literal("vertical"), + literal("default"), +]); diff --git a/src/utils/loader.ts b/src/utils/loader.ts index 10811eb3b..d95412444 100644 --- a/src/utils/loader.ts +++ b/src/utils/loader.ts @@ -1,21 +1,21 @@ // Hack to load ha-components needed for editor export const loadHaComponents = () => { - if (!customElements.get("ha-form")) { - (customElements.get("hui-button-card") as any)?.getConfigElement(); - } - if (!customElements.get("ha-entity-picker")) { - (customElements.get("hui-entities-card") as any)?.getConfigElement(); - } - if (!customElements.get("ha-card-conditions-editor")) { - (customElements.get("hui-conditional-card") as any)?.getConfigElement(); - } + if (!customElements.get("ha-form")) { + (customElements.get("hui-button-card") as any)?.getConfigElement(); + } + if (!customElements.get("ha-entity-picker")) { + (customElements.get("hui-entities-card") as any)?.getConfigElement(); + } + if (!customElements.get("ha-card-conditions-editor")) { + (customElements.get("hui-conditional-card") as any)?.getConfigElement(); + } }; export const loadCustomElement = async (name: string) => { - let Component = customElements.get(name) as T; - if (Component) { - return Component; - } - await customElements.whenDefined(name); - return customElements.get(name) as T; + let Component = customElements.get(name) as T; + if (Component) { + return Component; + } + await customElements.whenDefined(name); + return customElements.get(name) as T; }; diff --git a/src/utils/lovelace/chip-element-editor.ts b/src/utils/lovelace/chip-element-editor.ts index 5577ccb2b..89e0acabf 100644 --- a/src/utils/lovelace/chip-element-editor.ts +++ b/src/utils/lovelace/chip-element-editor.ts @@ -6,21 +6,21 @@ import { LovelaceChipEditor } from "./types"; @customElement("mushroom-chip-element-editor") export class MushroomChipElementEditor extends MushroomElementEditor { - protected get configElementType(): string | undefined { - return this.value?.type; - } - - protected async getConfigElement(): Promise { - const elClass = (await getChipElementClass(this.configElementType!)) as any; + protected get configElementType(): string | undefined { + return this.value?.type; + } - // Check if a GUI editor exists - if (elClass && elClass.getConfigElement) { - return elClass.getConfigElement(); - } + protected async getConfigElement(): Promise { + const elClass = (await getChipElementClass(this.configElementType!)) as any; - return undefined; + // Check if a GUI editor exists + if (elClass && elClass.getConfigElement) { + return elClass.getConfigElement(); } + + return undefined; + } } export const getChipElementClass = (type: string) => - customElements.get(computeChipComponentName(type)); + customElements.get(computeChipComponentName(type)); diff --git a/src/utils/lovelace/chip/chip-element.ts b/src/utils/lovelace/chip/chip-element.ts index 8fc6c055c..b20eb841d 100644 --- a/src/utils/lovelace/chip/chip-element.ts +++ b/src/utils/lovelace/chip/chip-element.ts @@ -1,36 +1,38 @@ import { PREFIX_NAME } from "../../../const"; import { LovelaceChip, LovelaceChipConfig } from "./types"; -export const createChipElement = (config: LovelaceChipConfig): LovelaceChip | undefined => { - try { - const tag = computeChipComponentName(config.type); - if (customElements.get(tag)) { - // @ts-ignore - const element = document.createElement(tag, config) as LovelaceChip; - element.setConfig(config); - return element; - } - // @ts-ignore - const element = document.createElement(tag) as LovelaceChip; - customElements.whenDefined(tag).then(() => { - try { - customElements.upgrade(element); - element.setConfig(config); - } catch (err: any) { - // Do nothing - } - }); - return element; - } catch (err) { - console.error(err); - return undefined; +export const createChipElement = ( + config: LovelaceChipConfig +): LovelaceChip | undefined => { + try { + const tag = computeChipComponentName(config.type); + if (customElements.get(tag)) { + // @ts-ignore + const element = document.createElement(tag, config) as LovelaceChip; + element.setConfig(config); + return element; } + // @ts-ignore + const element = document.createElement(tag) as LovelaceChip; + customElements.whenDefined(tag).then(() => { + try { + customElements.upgrade(element); + element.setConfig(config); + } catch (err: any) { + // Do nothing + } + }); + return element; + } catch (err) { + console.error(err); + return undefined; + } }; export function computeChipComponentName(type: string): string { - return `${PREFIX_NAME}-${type}-chip`; + return `${PREFIX_NAME}-${type}-chip`; } export function computeChipEditorComponentName(type: string): string { - return `${PREFIX_NAME}-${type}-chip-editor`; + return `${PREFIX_NAME}-${type}-chip-editor`; } diff --git a/src/utils/lovelace/chip/types.ts b/src/utils/lovelace/chip/types.ts index 017de21cc..92eacba65 100644 --- a/src/utils/lovelace/chip/types.ts +++ b/src/utils/lovelace/chip/types.ts @@ -2,122 +2,122 @@ import { ActionConfig, HomeAssistant } from "../../../ha"; import { Info } from "../../info"; export interface LovelaceChip extends HTMLElement { - hass?: HomeAssistant; - editMode?: boolean; - preview?: boolean; - setConfig(config: LovelaceChipConfig); + hass?: HomeAssistant; + editMode?: boolean; + preview?: boolean; + setConfig(config: LovelaceChipConfig); } export type ActionChipConfig = { - type: "action"; - icon?: string; - icon_color?: string; - tap_action?: ActionConfig; - hold_action?: ActionConfig; - double_tap_action?: ActionConfig; + type: "action"; + icon?: string; + icon_color?: string; + tap_action?: ActionConfig; + hold_action?: ActionConfig; + double_tap_action?: ActionConfig; }; export type AlarmControlPanelChipConfig = { - type: "alarm-control-panel"; - entity?: string; - name?: string; - content_info?: Info; - icon?: string; - icon_color?: string; - tap_action?: ActionConfig; - hold_action?: ActionConfig; - double_tap_action?: ActionConfig; + type: "alarm-control-panel"; + entity?: string; + name?: string; + content_info?: Info; + icon?: string; + icon_color?: string; + tap_action?: ActionConfig; + hold_action?: ActionConfig; + double_tap_action?: ActionConfig; }; export type BackChipConfig = { - type: "back"; - icon?: string; + type: "back"; + icon?: string; }; export type EntityChipConfig = { - type: "entity"; - entity?: string; - name?: string; - content_info?: Info; - icon?: string; - icon_color?: string; - use_entity_picture?: boolean; - tap_action?: ActionConfig; - hold_action?: ActionConfig; - double_tap_action?: ActionConfig; + type: "entity"; + entity?: string; + name?: string; + content_info?: Info; + icon?: string; + icon_color?: string; + use_entity_picture?: boolean; + tap_action?: ActionConfig; + hold_action?: ActionConfig; + double_tap_action?: ActionConfig; }; export type MenuChipConfig = { - type: "menu"; - icon?: string; + type: "menu"; + icon?: string; }; export type WeatherChipConfig = { - type: "weather"; - entity?: string; - tap_action?: ActionConfig; - hold_action?: ActionConfig; - double_tap_action?: ActionConfig; - show_temperature?: boolean; - show_conditions?: boolean; + type: "weather"; + entity?: string; + tap_action?: ActionConfig; + hold_action?: ActionConfig; + double_tap_action?: ActionConfig; + show_temperature?: boolean; + show_conditions?: boolean; }; export type TemplateChipConfig = { - type: "template"; - entity?: string; - hold_action?: ActionConfig; - tap_action?: ActionConfig; - double_tap_action?: ActionConfig; - content?: string; - icon?: string; - icon_color?: string; - picture?: string; - entity_id?: string | string[]; + type: "template"; + entity?: string; + hold_action?: ActionConfig; + tap_action?: ActionConfig; + double_tap_action?: ActionConfig; + content?: string; + icon?: string; + icon_color?: string; + picture?: string; + entity_id?: string | string[]; }; export interface ConditionalChipConfig { - type: "conditional"; - chip?: LovelaceChipConfig; - conditions: any[]; + type: "conditional"; + chip?: LovelaceChipConfig; + conditions: any[]; } export type LightChipConfig = { - type: "light"; - entity?: string; - name?: string; - content_info?: Info; - icon?: string; - use_light_color?: boolean; - hold_action?: ActionConfig; - tap_action?: ActionConfig; - double_tap_action?: ActionConfig; + type: "light"; + entity?: string; + name?: string; + content_info?: Info; + icon?: string; + use_light_color?: boolean; + hold_action?: ActionConfig; + tap_action?: ActionConfig; + double_tap_action?: ActionConfig; }; export type SpacerChipConfig = { - type: "spacer"; + type: "spacer"; }; export type LovelaceChipConfig = - | ActionChipConfig - | AlarmControlPanelChipConfig - | BackChipConfig - | EntityChipConfig - | MenuChipConfig - | WeatherChipConfig - | TemplateChipConfig - | ConditionalChipConfig - | LightChipConfig - | SpacerChipConfig; + | ActionChipConfig + | AlarmControlPanelChipConfig + | BackChipConfig + | EntityChipConfig + | MenuChipConfig + | WeatherChipConfig + | TemplateChipConfig + | ConditionalChipConfig + | LightChipConfig + | SpacerChipConfig; export const CHIP_LIST: LovelaceChipConfig["type"][] = [ - "action", - "alarm-control-panel", - "back", - "conditional", - "entity", - "light", - "menu", - "spacer", - "template", - "weather", + "action", + "alarm-control-panel", + "back", + "conditional", + "entity", + "light", + "menu", + "spacer", + "template", + "weather", ]; diff --git a/src/utils/lovelace/editor/types.ts b/src/utils/lovelace/editor/types.ts index af13fde91..606a1141f 100644 --- a/src/utils/lovelace/editor/types.ts +++ b/src/utils/lovelace/editor/types.ts @@ -1,85 +1,90 @@ -import { ActionConfig, LovelaceCardConfig, LovelaceViewConfig, ShowViewConfig } from "../../../ha"; +import { + ActionConfig, + LovelaceCardConfig, + LovelaceViewConfig, + ShowViewConfig, +} from "../../../ha"; import { LovelaceChipConfig } from "../chip/types"; export interface YamlChangedEvent extends Event { - detail: { - yaml: string; - }; + detail: { + yaml: string; + }; } export interface GUIModeChangedEvent { - guiMode: boolean; - guiModeAvailable: boolean; + guiMode: boolean; + guiModeAvailable: boolean; } export interface ViewEditEvent extends Event { - detail: { - config: LovelaceViewConfig; - }; + detail: { + config: LovelaceViewConfig; + }; } export interface ViewVisibilityChangeEvent { - visible: ShowViewConfig[]; + visible: ShowViewConfig[]; } export interface ConfigValue { - format: "json" | "yaml"; - value?: string | LovelaceCardConfig; + format: "json" | "yaml"; + value?: string | LovelaceCardConfig; } export interface ConfigError { - type: string; - message: string; + type: string; + message: string; } export interface EntityConfig { - entity: string; - type?: string; - name?: string; - icon?: string; - image?: string; + entity: string; + type?: string; + name?: string; + icon?: string; + image?: string; } export interface EntitiesEditorEvent { - detail?: { - entities?: EntityConfig[]; - item?: any; - }; - target?: EventTarget; + detail?: { + entities?: EntityConfig[]; + item?: any; + }; + target?: EventTarget; } export interface EditorTarget extends EventTarget { - value?: string; - index?: number; - checked?: boolean; - configValue?: string; - type?: HTMLInputElement["type"]; - config: ActionConfig; + value?: string; + index?: number; + checked?: boolean; + configValue?: string; + type?: HTMLInputElement["type"]; + config: ActionConfig; } export interface Card { - type: string; - name?: string; - description?: string; - showElement?: boolean; - isCustom?: boolean; + type: string; + name?: string; + description?: string; + showElement?: boolean; + isCustom?: boolean; } export interface HeaderFooter { - type: string; - icon?: string; + type: string; + icon?: string; } export interface CardPickTarget extends EventTarget { - config: LovelaceCardConfig; + config: LovelaceCardConfig; } export interface SubElementEditorConfig { - index?: number; - elementConfig?: LovelaceChipConfig; - type: string; + index?: number; + elementConfig?: LovelaceChipConfig; + type: string; } export interface EditSubElementEvent { - subElementConfig: SubElementEditorConfig; + subElementConfig: SubElementEditorConfig; } diff --git a/src/utils/lovelace/element-editor.ts b/src/utils/lovelace/element-editor.ts index 7e2f4b35e..84d452846 100644 --- a/src/utils/lovelace/element-editor.ts +++ b/src/utils/lovelace/element-editor.ts @@ -1,14 +1,21 @@ import { dump, load } from "js-yaml"; -import { css, CSSResultGroup, html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; import { property, query, state } from "lit/decorators.js"; import { - computeRTL, - deepEqual, - fireEvent, - handleStructError, - HomeAssistant, - LovelaceCardConfig, - LovelaceConfig, + computeRTL, + deepEqual, + fireEvent, + handleStructError, + HomeAssistant, + LovelaceCardConfig, + LovelaceConfig, } from "../../ha"; import { LovelaceChipConfig } from "./chip/types"; import { EditSubElementEvent, GUIModeChangedEvent } from "./editor/types"; @@ -16,360 +23,372 @@ import { GUISupportError } from "./gui-support-error"; import { LovelaceGenericElementEditor } from "./types"; export interface ConfigChangedEvent { - config: LovelaceCardConfig | LovelaceChipConfig; - error?: string; - guiModeAvailable?: boolean; + config: LovelaceCardConfig | LovelaceChipConfig; + error?: string; + guiModeAvailable?: boolean; } declare global { - interface HASSDomEvents { - // @ts-ignore - "config-changed": ConfigChangedEvent; - "GUImode-changed": GUIModeChangedEvent; - "edit-detail-element": EditSubElementEvent; - } + interface HASSDomEvents { + // @ts-ignore + "config-changed": ConfigChangedEvent; + "GUImode-changed": GUIModeChangedEvent; + "edit-detail-element": EditSubElementEvent; + } } export interface UIConfigChangedEvent extends Event { - detail: { - config: LovelaceCardConfig | LovelaceChipConfig; - }; + detail: { + config: LovelaceCardConfig | LovelaceChipConfig; + }; } export abstract class MushroomElementEditor extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public lovelace?: LovelaceConfig; + @property({ attribute: false }) public lovelace?: LovelaceConfig; - @state() private _yaml?: string; + @state() private _yaml?: string; - @state() private _config?: T; + @state() private _config?: T; - @state() private _configElement?: LovelaceGenericElementEditor; + @state() private _configElement?: LovelaceGenericElementEditor; - @state() private _configElementType?: string; + @state() private _configElementType?: string; - @state() private _guiMode = true; + @state() private _guiMode = true; - // Error: Configuration broken - do not save - @state() private _errors?: string[]; + // Error: Configuration broken - do not save + @state() private _errors?: string[]; - // Warning: GUI editor can't handle configuration - ok to save - @state() private _warnings?: string[]; + // Warning: GUI editor can't handle configuration - ok to save + @state() private _warnings?: string[]; - @state() private _guiSupported?: boolean; + @state() private _guiSupported?: boolean; - @state() private _loading = false; + @state() private _loading = false; - @query("ha-code-editor") _yamlEditor?: any /**HaCodeEditor**/; + @query("ha-code-editor") _yamlEditor?: any /**HaCodeEditor**/; - public get yaml(): string { - if (!this._yaml) { - this._yaml = dump(this._config); - } - return this._yaml || ""; + public get yaml(): string { + if (!this._yaml) { + this._yaml = dump(this._config); } - - public set yaml(_yaml: string) { - this._yaml = _yaml; - try { - this._config = load(this.yaml) as any; - this._errors = undefined; - } catch (err: any) { - this._errors = [err.message]; - } - this._setConfig(); + return this._yaml || ""; + } + + public set yaml(_yaml: string) { + this._yaml = _yaml; + try { + this._config = load(this.yaml) as any; + this._errors = undefined; + } catch (err: any) { + this._errors = [err.message]; } + this._setConfig(); + } - public get value(): T | undefined { - return this._config; - } + public get value(): T | undefined { + return this._config; + } - public set value(config: T | undefined) { - if (this._config && deepEqual(config, this._config)) { - return; - } - this._config = config; - this._yaml = undefined; - this._errors = undefined; - this._setConfig(); + public set value(config: T | undefined) { + if (this._config && deepEqual(config, this._config)) { + return; } - - private _setConfig(): void { - if (!this._errors) { - try { - this._updateConfigElement(); - } catch (err: any) { - this._errors = [err.message]; - } - } - - fireEvent(this, "config-changed", { - config: this.value! as any, - // @ts-ignore - error: this._errors?.join(", "), - guiModeAvailable: !(this.hasWarning || this.hasError || this._guiSupported === false), - }); + this._config = config; + this._yaml = undefined; + this._errors = undefined; + this._setConfig(); + } + + private _setConfig(): void { + if (!this._errors) { + try { + this._updateConfigElement(); + } catch (err: any) { + this._errors = [err.message]; + } } - public get hasWarning(): boolean { - return this._warnings !== undefined && this._warnings.length > 0; + fireEvent(this, "config-changed", { + config: this.value! as any, + // @ts-ignore + error: this._errors?.join(", "), + guiModeAvailable: !( + this.hasWarning || + this.hasError || + this._guiSupported === false + ), + }); + } + + public get hasWarning(): boolean { + return this._warnings !== undefined && this._warnings.length > 0; + } + + public get hasError(): boolean { + return this._errors !== undefined && this._errors.length > 0; + } + + public get GUImode(): boolean { + return this._guiMode; + } + + public set GUImode(guiMode: boolean) { + this._guiMode = guiMode; + fireEvent(this as HTMLElement, "GUImode-changed", { + guiMode, + guiModeAvailable: !( + this.hasWarning || + this.hasError || + this._guiSupported === false + ), + }); + } + + public toggleMode() { + this.GUImode = !this.GUImode; + } + + public focusYamlEditor() { + if (this._configElement?.focusYamlEditor) { + this._configElement.focusYamlEditor(); } - - public get hasError(): boolean { - return this._errors !== undefined && this._errors.length > 0; + if (!this._yamlEditor?.codemirror) { + return; } - - public get GUImode(): boolean { - return this._guiMode; + this._yamlEditor.codemirror.focus(); + } + + protected async getConfigElement(): Promise< + LovelaceGenericElementEditor | undefined + > { + return undefined; + } + + protected get configElementType(): string | undefined { + return this.value ? (this.value as any).type : undefined; + } + + protected render(): TemplateResult { + return html` +
+ ${this.GUImode + ? html` +
+ ${this._loading + ? html` + + ` + : this._configElement} +
+ ` + : html` +
+ +
+ `} + ${this._guiSupported === false && this.configElementType + ? html` +
+ ${this.hass.localize( + "ui.errors.config.editor_not_available", + "type", + this.configElementType + )} +
+ ` + : ""} + ${this.hasError + ? html` +
+ ${this.hass.localize("ui.errors.config.error_detected")}: +
+
    + ${this._errors!.map((error) => html`
  • ${error}
  • `)} +
+
+ ` + : ""} + ${this.hasWarning + ? html` + + ${this._warnings!.length > 0 && this._warnings![0] !== undefined + ? html` +
    + ${this._warnings!.map( + (warning) => html`
  • ${warning}
  • ` + )} +
+ ` + : undefined} + ${this.hass.localize("ui.errors.config.edit_in_yaml_supported")} +
+ ` + : ""} +
+ `; + } + + protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + + if (this._configElement && changedProperties.has("hass")) { + this._configElement.hass = this.hass; } - - public set GUImode(guiMode: boolean) { - this._guiMode = guiMode; - fireEvent(this as HTMLElement, "GUImode-changed", { - guiMode, - guiModeAvailable: !(this.hasWarning || this.hasError || this._guiSupported === false), - }); + if ( + this._configElement && + "lovelace" in this._configElement && + changedProperties.has("lovelace") + ) { + this._configElement.lovelace = this.lovelace; } - - public toggleMode() { - this.GUImode = !this.GUImode; + } + + private _handleUIConfigChanged(ev: UIConfigChangedEvent) { + ev.stopPropagation(); + const config = ev.detail.config; + this.value = config as unknown as T; + } + + private _handleYAMLChanged(ev: CustomEvent) { + ev.stopPropagation(); + const newYaml = ev.detail.value; + if (newYaml !== this.yaml) { + this.yaml = newYaml; } + } - public focusYamlEditor() { - if (this._configElement?.focusYamlEditor) { - this._configElement.focusYamlEditor(); - } - if (!this._yamlEditor?.codemirror) { - return; - } - this._yamlEditor.codemirror.focus(); + private async _updateConfigElement(): Promise { + if (!this.value) { + return; } - protected async getConfigElement(): Promise { - return undefined; - } + let configElement: LovelaceGenericElementEditor | undefined; - protected get configElementType(): string | undefined { - return this.value ? (this.value as any).type : undefined; - } + try { + this._errors = undefined; + this._warnings = undefined; - protected render(): TemplateResult { - return html` -
- ${this.GUImode - ? html` -
- ${this._loading - ? html` - - ` - : this._configElement} -
- ` - : html` -
- -
- `} - ${this._guiSupported === false && this.configElementType - ? html` -
- ${this.hass.localize( - "ui.errors.config.editor_not_available", - "type", - this.configElementType - )} -
- ` - : ""} - ${this.hasError - ? html` -
- ${this.hass.localize("ui.errors.config.error_detected")}: -
-
    - ${this._errors!.map((error) => html`
  • ${error}
  • `)} -
-
- ` - : ""} - ${this.hasWarning - ? html` - - ${this._warnings!.length > 0 && this._warnings![0] !== undefined - ? html` -
    - ${this._warnings!.map( - (warning) => html`
  • ${warning}
  • ` - )} -
- ` - : undefined} - ${this.hass.localize("ui.errors.config.edit_in_yaml_supported")} -
- ` - : ""} -
- `; - } + if (this._configElementType !== this.configElementType) { + // If the type has changed, we need to load a new GUI editor + this._guiSupported = undefined; + this._configElement = undefined; - protected updated(changedProperties: PropertyValues) { - super.updated(changedProperties); - - if (this._configElement && changedProperties.has("hass")) { - this._configElement.hass = this.hass; - } - if ( - this._configElement && - "lovelace" in this._configElement && - changedProperties.has("lovelace") - ) { - this._configElement.lovelace = this.lovelace; + if (!this.configElementType) { + throw new Error( + this.hass.localize("ui.errors.config.no_type_provided") + ); } - } - private _handleUIConfigChanged(ev: UIConfigChangedEvent) { - ev.stopPropagation(); - const config = ev.detail.config; - this.value = config as unknown as T; - } + this._configElementType = this.configElementType; - private _handleYAMLChanged(ev: CustomEvent) { - ev.stopPropagation(); - const newYaml = ev.detail.value; - if (newYaml !== this.yaml) { - this.yaml = newYaml; - } - } + this._loading = true; + configElement = await this.getConfigElement(); - private async _updateConfigElement(): Promise { - if (!this.value) { - return; - } + if (configElement) { + configElement.hass = this.hass; + if ("lovelace" in configElement) { + configElement.lovelace = this.lovelace; + } + configElement.addEventListener("config-changed", (ev) => + this._handleUIConfigChanged(ev as UIConfigChangedEvent) + ); - let configElement: LovelaceGenericElementEditor | undefined; + this._configElement = configElement; + this._guiSupported = true; + } + } + if (this._configElement) { + // Setup GUI editor and check that it can handle the current config try { - this._errors = undefined; - this._warnings = undefined; - - if (this._configElementType !== this.configElementType) { - // If the type has changed, we need to load a new GUI editor - this._guiSupported = undefined; - this._configElement = undefined; - - if (!this.configElementType) { - throw new Error(this.hass.localize("ui.errors.config.no_type_provided")); - } - - this._configElementType = this.configElementType; - - this._loading = true; - configElement = await this.getConfigElement(); - - if (configElement) { - configElement.hass = this.hass; - if ("lovelace" in configElement) { - configElement.lovelace = this.lovelace; - } - configElement.addEventListener("config-changed", (ev) => - this._handleUIConfigChanged(ev as UIConfigChangedEvent) - ); - - this._configElement = configElement; - this._guiSupported = true; - } - } - - if (this._configElement) { - // Setup GUI editor and check that it can handle the current config - try { - this._configElement.setConfig(this.value); - } catch (err: any) { - const msgs = handleStructError(this.hass, err); - throw new GUISupportError( - "Config is not supported", - msgs.warnings, - msgs.errors - ); - } - } else { - this.GUImode = false; - } + this._configElement.setConfig(this.value); } catch (err: any) { - if (err instanceof GUISupportError) { - this._warnings = err.warnings ?? [err.message]; - this._errors = err.errors || undefined; - } else { - this._errors = [err.message]; - } - this.GUImode = false; - } finally { - this._loading = false; + const msgs = handleStructError(this.hass, err); + throw new GUISupportError( + "Config is not supported", + msgs.warnings, + msgs.errors + ); } + } else { + this.GUImode = false; + } + } catch (err: any) { + if (err instanceof GUISupportError) { + this._warnings = err.warnings ?? [err.message]; + this._errors = err.errors || undefined; + } else { + this._errors = [err.message]; + } + this.GUImode = false; + } finally { + this._loading = false; } - - private _ignoreKeydown(ev: KeyboardEvent) { - ev.stopPropagation(); - } - - static get styles(): CSSResultGroup { - return css` - :host { - display: flex; - } - .wrapper { - width: 100%; - } - .gui-editor, - .yaml-editor { - padding: 8px 0px; - } - ha-code-editor { - --code-mirror-max-height: calc(100vh - 245px); - } - .error, - .warning, - .info { - word-break: break-word; - margin-top: 8px; - } - .error { - color: var(--error-color); - } - .warning { - color: var(--warning-color); - } - .warning ul, - .error ul { - margin: 4px 0; - } - .warning li, - .error li { - white-space: pre-wrap; - } - ha-circular-progress { - display: block; - margin: auto; - } - `; - } + } + + private _ignoreKeydown(ev: KeyboardEvent) { + ev.stopPropagation(); + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: flex; + } + .wrapper { + width: 100%; + } + .gui-editor, + .yaml-editor { + padding: 8px 0px; + } + ha-code-editor { + --code-mirror-max-height: calc(100vh - 245px); + } + .error, + .warning, + .info { + word-break: break-word; + margin-top: 8px; + } + .error { + color: var(--error-color); + } + .warning { + color: var(--warning-color); + } + .warning ul, + .error ul { + margin: 4px 0; + } + .warning li, + .error li { + white-space: pre-wrap; + } + ha-circular-progress { + display: block; + margin: auto; + } + `; + } } diff --git a/src/utils/lovelace/gui-support-error.ts b/src/utils/lovelace/gui-support-error.ts index 93cbaa52e..7d1b32534 100644 --- a/src/utils/lovelace/gui-support-error.ts +++ b/src/utils/lovelace/gui-support-error.ts @@ -1,12 +1,12 @@ export class GUISupportError extends Error { - public warnings?: string[]; + public warnings?: string[]; - public errors?: string[]; + public errors?: string[]; - constructor(message: string, warnings?: string[], errors?: string[]) { - super(message); - this.name = "GUISupportError"; - this.warnings = warnings; - this.errors = errors; - } + constructor(message: string, warnings?: string[], errors?: string[]) { + super(message); + this.name = "GUISupportError"; + this.warnings = warnings; + this.errors = errors; + } } diff --git a/src/utils/lovelace/sub-element-editor.ts b/src/utils/lovelace/sub-element-editor.ts index 83095f88c..5fc659ffc 100644 --- a/src/utils/lovelace/sub-element-editor.ts +++ b/src/utils/lovelace/sub-element-editor.ts @@ -8,107 +8,107 @@ import { GUIModeChangedEvent, SubElementEditorConfig } from "./editor/types"; import type { MushroomElementEditor } from "./element-editor"; declare global { - interface HASSDomEvents { - "go-back": undefined; - } + interface HASSDomEvents { + "go-back": undefined; + } } @customElement("mushroom-sub-element-editor") export class MushroomSubElementEditor extends LitElement { - public hass!: HomeAssistant; + public hass!: HomeAssistant; - @property({ attribute: false }) public config!: SubElementEditorConfig; + @property({ attribute: false }) public config!: SubElementEditorConfig; - @state() private _guiModeAvailable = true; + @state() private _guiModeAvailable = true; - @state() private _guiMode = true; + @state() private _guiMode = true; - @query(".editor") - private _editorElement?: MushroomElementEditor; + @query(".editor") + private _editorElement?: MushroomElementEditor; - protected render(): TemplateResult { - const customLocalize = setupCustomlocalize(this.hass); + protected render(): TemplateResult { + const customLocalize = setupCustomlocalize(this.hass); - return html` -
-
- - - - ${customLocalize(`editor.chip.sub_element_editor.title`)} -
- - ${this.hass.localize( - this._guiMode - ? "ui.panel.lovelace.editor.edit_card.show_code_editor" - : "ui.panel.lovelace.editor.edit_card.show_visual_editor" - )} - -
- ${this.config.type === "chip" - ? html` - - ` - : ""} - `; - } + return html` +
+
+ + + + ${customLocalize(`editor.chip.sub_element_editor.title`)} +
+ + ${this.hass.localize( + this._guiMode + ? "ui.panel.lovelace.editor.edit_card.show_code_editor" + : "ui.panel.lovelace.editor.edit_card.show_visual_editor" + )} + +
+ ${this.config.type === "chip" + ? html` + + ` + : ""} + `; + } - private _goBack(): void { - fireEvent(this, "go-back"); - } + private _goBack(): void { + fireEvent(this, "go-back"); + } - private _toggleMode(): void { - this._editorElement?.toggleMode(); - } + private _toggleMode(): void { + this._editorElement?.toggleMode(); + } - private _handleGUIModeChanged(ev: HASSDomEvent): void { - ev.stopPropagation(); - this._guiMode = ev.detail.guiMode; - this._guiModeAvailable = ev.detail.guiModeAvailable; - } + private _handleGUIModeChanged(ev: HASSDomEvent): void { + ev.stopPropagation(); + this._guiMode = ev.detail.guiMode; + this._guiModeAvailable = ev.detail.guiModeAvailable; + } - private _handleConfigChanged(ev: CustomEvent): void { - this._guiModeAvailable = ev.detail.guiModeAvailable; - } + private _handleConfigChanged(ev: CustomEvent): void { + this._guiModeAvailable = ev.detail.guiModeAvailable; + } - static get styles(): CSSResultGroup { - return css` - .header { - display: flex; - justify-content: space-between; - align-items: center; - } - .back-title { - display: flex; - align-items: center; - font-size: 18px; - } - ha-icon { - display: flex; - align-items: center; - justify-content: center; - } - `; - } + static get styles(): CSSResultGroup { + return css` + .header { + display: flex; + justify-content: space-between; + align-items: center; + } + .back-title { + display: flex; + align-items: center; + font-size: 18px; + } + ha-icon { + display: flex; + align-items: center; + justify-content: center; + } + `; + } } declare global { - interface HTMLElementTagNameMap { - "hui-sub-element-editor": MushroomSubElementEditor; - } + interface HTMLElementTagNameMap { + "hui-sub-element-editor": MushroomSubElementEditor; + } } diff --git a/src/utils/lovelace/types.ts b/src/utils/lovelace/types.ts index bda09303c..5e1799eeb 100644 --- a/src/utils/lovelace/types.ts +++ b/src/utils/lovelace/types.ts @@ -1,18 +1,23 @@ -import { Condition, HomeAssistant, LovelaceCardConfig, LovelaceConfig } from "../../ha"; +import { + Condition, + HomeAssistant, + LovelaceCardConfig, + LovelaceConfig, +} from "../../ha"; import { LovelaceChipConfig } from "./chip/types"; export interface LovelaceChipEditor extends LovelaceGenericElementEditor { - setConfig(config: LovelaceChipConfig): void; + setConfig(config: LovelaceChipConfig): void; } export interface LovelaceGenericElementEditor extends HTMLElement { - hass?: HomeAssistant; - lovelace?: LovelaceConfig; - setConfig(config: any): void; - focusYamlEditor?: () => void; + hass?: HomeAssistant; + lovelace?: LovelaceConfig; + setConfig(config: any): void; + focusYamlEditor?: () => void; } export interface ConditionalCardConfig extends LovelaceCardConfig { - card: LovelaceCardConfig; - conditions: Condition[]; + card: LovelaceCardConfig; + conditions: Condition[]; } diff --git a/src/utils/theme.ts b/src/utils/theme.ts index 16071a8fb..6307cfa58 100644 --- a/src/utils/theme.ts +++ b/src/utils/theme.ts @@ -1,156 +1,222 @@ import { css } from "lit"; export const themeVariables = css` - --spacing: var(--mush-spacing, 12px); - - /* Title */ - --title-padding: var(--mush-title-padding, 24px 12px 8px); - --title-spacing: var(--mush-title-spacing, 8px); - --title-font-size: var(--mush-title-font-size, 24px); - --title-font-weight: var(--mush-title-font-weight, normal); - --title-line-height: var(--mush-title-line-height, 32px); - --title-color: var(--mush-title-color, var(--primary-text-color)); - --title-letter-spacing: var(--mush-title-letter-spacing, -0.288px); - --subtitle-font-size: var(--mush-subtitle-font-size, 16px); - --subtitle-font-weight: var(--mush-subtitle-font-weight, normal); - --subtitle-line-height: var(--mush-subtitle-line-height, 24px); - --subtitle-color: var(--mush-subtitle-color, var(--secondary-text-color)); - --subtitle-letter-spacing: var(--mush-subtitle-letter-spacing, 0px); - - /* Card */ - --card-primary-font-size: var(--mush-card-primary-font-size, 14px); - --card-secondary-font-size: var(--mush-card-secondary-font-size, 12px); - --card-primary-font-weight: var(--mush-card-primary-font-weight, 500); - --card-secondary-font-weight: var(--mush-card-secondary-font-weight, 400); - --card-primary-line-height: var(--mush-card-primary-line-height, 20px); - --card-secondary-line-height: var(--mush-card-secondary-line-height, 16px); - --card-primary-color: var(--mush-card-primary-color, var(--primary-text-color)); - --card-secondary-color: var(--mush-card-secondary-color, var(--primary-text-color)); - --card-primary-letter-spacing: var(--mush-card-primary-letter-spacing, 0.1px); - --card-secondary-letter-spacing: var(--mush-card-secondary-letter-spacing, 0.4px); - - /* Chips */ - --chip-spacing: var(--mush-chip-spacing, 8px); - --chip-padding: var(--mush-chip-padding, 0 0.25em); - --chip-height: var(--mush-chip-height, 36px); - --chip-border-radius: var(--mush-chip-border-radius, 19px); - --chip-border-width: var(--mush-chip-border-width, var(--ha-card-border-width, 1px)); - --chip-border-color: var( - --mush-chip-border-color, - var(--ha-card-border-color, var(--divider-color)) - ); - --chip-box-shadow: var(--mush-chip-box-shadow, var(--ha-card-box-shadow, "none")); - --chip-font-size: var(--mush-chip-font-size, 0.3em); - --chip-font-weight: var(--mush-chip-font-weight, bold); - --chip-icon-size: var(--mush-chip-icon-size, 0.5em); - --chip-avatar-padding: var(--mush-chip-avatar-padding, 0.1em); - --chip-avatar-border-radius: var(--mush-chip-avatar-border-radius, 50%); - --chip-background: var( - --mush-chip-background, - var(--ha-card-background, var(--card-background-color, white)) - ); - /* Controls */ - --control-border-radius: var(--mush-control-border-radius, 12px); - --control-height: var(--mush-control-height, 40px); - --control-button-ratio: var(--mush-control-button-ratio, 1); - --control-icon-size: var(--mush-control-icon-size, 0.5em); - - /* Slider */ - --slider-threshold: var(--mush-slider-threshold); - - /* Input Number */ - --input-number-debounce: var(--mush-input-number-debounce); - - /* Layout */ - --layout-align: var(--mush-layout-align, center); - - /* Badge */ - --badge-size: var(--mush-badge-size, 16px); - --badge-icon-size: var(--mush-badge-icon-size, 0.75em); - --badge-border-radius: var(--mush-badge-border-radius, 50%); - - /* Icon */ - --icon-border-radius: var(--mush-icon-border-radius, 50%); - --icon-size: var(--mush-icon-size, 40px); - --icon-symbol-size: var(--mush-icon-symbol-size, 0.6em); + --spacing: var(--mush-spacing, 12px); + + /* Title */ + --title-padding: var(--mush-title-padding, 24px 12px 8px); + --title-spacing: var(--mush-title-spacing, 8px); + --title-font-size: var(--mush-title-font-size, 24px); + --title-font-weight: var(--mush-title-font-weight, normal); + --title-line-height: var(--mush-title-line-height, 32px); + --title-color: var(--mush-title-color, var(--primary-text-color)); + --title-letter-spacing: var(--mush-title-letter-spacing, -0.288px); + --subtitle-font-size: var(--mush-subtitle-font-size, 16px); + --subtitle-font-weight: var(--mush-subtitle-font-weight, normal); + --subtitle-line-height: var(--mush-subtitle-line-height, 24px); + --subtitle-color: var(--mush-subtitle-color, var(--secondary-text-color)); + --subtitle-letter-spacing: var(--mush-subtitle-letter-spacing, 0px); + + /* Card */ + --card-primary-font-size: var(--mush-card-primary-font-size, 14px); + --card-secondary-font-size: var(--mush-card-secondary-font-size, 12px); + --card-primary-font-weight: var(--mush-card-primary-font-weight, 500); + --card-secondary-font-weight: var(--mush-card-secondary-font-weight, 400); + --card-primary-line-height: var(--mush-card-primary-line-height, 20px); + --card-secondary-line-height: var(--mush-card-secondary-line-height, 16px); + --card-primary-color: var( + --mush-card-primary-color, + var(--primary-text-color) + ); + --card-secondary-color: var( + --mush-card-secondary-color, + var(--primary-text-color) + ); + --card-primary-letter-spacing: var(--mush-card-primary-letter-spacing, 0.1px); + --card-secondary-letter-spacing: var( + --mush-card-secondary-letter-spacing, + 0.4px + ); + + /* Chips */ + --chip-spacing: var(--mush-chip-spacing, 8px); + --chip-padding: var(--mush-chip-padding, 0 0.25em); + --chip-height: var(--mush-chip-height, 36px); + --chip-border-radius: var(--mush-chip-border-radius, 19px); + --chip-border-width: var( + --mush-chip-border-width, + var(--ha-card-border-width, 1px) + ); + --chip-border-color: var( + --mush-chip-border-color, + var(--ha-card-border-color, var(--divider-color)) + ); + --chip-box-shadow: var( + --mush-chip-box-shadow, + var(--ha-card-box-shadow, "none") + ); + --chip-font-size: var(--mush-chip-font-size, 0.3em); + --chip-font-weight: var(--mush-chip-font-weight, bold); + --chip-icon-size: var(--mush-chip-icon-size, 0.5em); + --chip-avatar-padding: var(--mush-chip-avatar-padding, 0.1em); + --chip-avatar-border-radius: var(--mush-chip-avatar-border-radius, 50%); + --chip-background: var( + --mush-chip-background, + var(--ha-card-background, var(--card-background-color, white)) + ); + /* Controls */ + --control-border-radius: var(--mush-control-border-radius, 12px); + --control-height: var(--mush-control-height, 40px); + --control-button-ratio: var(--mush-control-button-ratio, 1); + --control-icon-size: var(--mush-control-icon-size, 0.5em); + + /* Slider */ + --slider-threshold: var(--mush-slider-threshold); + + /* Input Number */ + --input-number-debounce: var(--mush-input-number-debounce); + + /* Layout */ + --layout-align: var(--mush-layout-align, center); + + /* Badge */ + --badge-size: var(--mush-badge-size, 16px); + --badge-icon-size: var(--mush-badge-icon-size, 0.75em); + --badge-border-radius: var(--mush-badge-border-radius, 50%); + + /* Icon */ + --icon-border-radius: var(--mush-icon-border-radius, 50%); + --icon-size: var(--mush-icon-size, 40px); + --icon-symbol-size: var(--mush-icon-symbol-size, 0.6em); `; export const themeColorCss = css` - /* RGB */ - /* Standard colors */ - --rgb-red: var(--mush-rgb-red, var(--default-red)); - --rgb-pink: var(--mush-rgb-pink, var(--default-pink)); - --rgb-purple: var(--mush-rgb-purple, var(--default-purple)); - --rgb-deep-purple: var(--mush-rgb-deep-purple, var(--default-deep-purple)); - --rgb-indigo: var(--mush-rgb-indigo, var(--default-indigo)); - --rgb-blue: var(--mush-rgb-blue, var(--default-blue)); - --rgb-light-blue: var(--mush-rgb-light-blue, var(--default-light-blue)); - --rgb-cyan: var(--mush-rgb-cyan, var(--default-cyan)); - --rgb-teal: var(--mush-rgb-teal, var(--default-teal)); - --rgb-green: var(--mush-rgb-green, var(--default-green)); - --rgb-light-green: var(--mush-rgb-light-green, var(--default-light-green)); - --rgb-lime: var(--mush-rgb-lime, var(--default-lime)); - --rgb-yellow: var(--mush-rgb-yellow, var(--default-yellow)); - --rgb-amber: var(--mush-rgb-amber, var(--default-amber)); - --rgb-orange: var(--mush-rgb-orange, var(--default-orange)); - --rgb-deep-orange: var(--mush-rgb-deep-orange, var(--default-deep-orange)); - --rgb-brown: var(--mush-rgb-brown, var(--default-brown)); - --rgb-light-grey: var(--mush-rgb-light-grey, var(--default-light-grey)); - --rgb-grey: var(--mush-rgb-grey, var(--default-grey)); - --rgb-dark-grey: var(--mush-rgb-dark-grey, var(--default-dark-grey)); - --rgb-blue-grey: var(--mush-rgb-blue-grey, var(--default-blue-grey)); - --rgb-black: var(--mush-rgb-black, var(--default-black)); - --rgb-white: var(--mush-rgb-white, var(--default-white)); - --rgb-disabled: var(--mush-rgb-disabled, var(--default-disabled)); - - /* Action colors */ - --rgb-info: var(--mush-rgb-info, var(--rgb-blue)); - --rgb-success: var(--mush-rgb-success, var(--rgb-green)); - --rgb-warning: var(--mush-rgb-warning, var(--rgb-orange)); - --rgb-danger: var(--mush-rgb-danger, var(--rgb-red)); - - /* State colors */ - --rgb-state-vacuum: var(--mush-rgb-state-vacuum, var(--rgb-teal)); - --rgb-state-fan: var(--mush-rgb-state-fan, var(--rgb-green)); - --rgb-state-light: var(--mush-rgb-state-light, var(--rgb-orange)); - --rgb-state-entity: var(--mush-rgb-state-entity, var(--rgb-blue)); - --rgb-state-media-player: var(--mush-rgb-state-media-player, var(--rgb-indigo)); - --rgb-state-lock: var(--mush-rgb-state-lock, var(--rgb-blue)); - --rgb-state-number: var(--mush-rgb-state-number, var(--rgb-blue)); - --rgb-state-humidifier: var(--mush-rgb-state-humidifier, var(--rgb-purple)); - - /* State alarm colors */ - --rgb-state-alarm-disarmed: var(--mush-rgb-state-alarm-disarmed, var(--rgb-info)); - --rgb-state-alarm-armed: var(--mush-rgb-state-alarm-armed, var(--rgb-success)); - --rgb-state-alarm-triggered: var(--mush-rgb-state-alarm-triggered, var(--rgb-danger)); - - /* State person colors */ - --rgb-state-person-home: var(--mush-rgb-state-person-home, var(--rgb-success)); - --rgb-state-person-not-home: var(--mush-rgb-state-person-not-home, var(--rgb-danger)); - --rgb-state-person-zone: var(--mush-rgb-state-person-zone, var(--rgb-info)); - --rgb-state-person-unknown: var(--mush-rgb-state-person-unknown, var(--rgb-grey)); - - /* State update colors */ - --rgb-state-update-on: var(--mush-rgb-state-update-on, var(--rgb-orange)); - --rgb-state-update-off: var(--mush-rgb-update-off, var(--rgb-green)); - --rgb-state-update-installing: var(--mush-rgb-update-installing, var(--rgb-blue)); - - /* State lock colors */ - --rgb-state-lock-locked: var(--mush-rgb-state-lock-locked, var(--rgb-green)); - --rgb-state-lock-unlocked: var(--mush-rgb-state-lock-unlocked, var(--rgb-red)); - --rgb-state-lock-pending: var(--mush-rgb-state-lock-pending, var(--rgb-orange)); - - /* State cover colors */ - --rgb-state-cover-open: var(--mush-rgb-state-cover-open, var(--rgb-blue)); - --rgb-state-cover-closed: var(--mush-rgb-state-cover-closed, var(--rgb-disabled)); - - /* State climate colors */ - --rgb-state-climate-auto: var(--mush-rgb-state-climate-auto, var(--rgb-green)); - --rgb-state-climate-cool: var(--mush-rgb-state-climate-cool, var(--rgb-blue)); - --rgb-state-climate-dry: var(--mush-rgb-state-climate-dry, var(--rgb-orange)); - --rgb-state-climate-fan-only: var(--mush-rgb-state-climate-fan-only, var(--rgb-teal)); - --rgb-state-climate-heat: var(--mush-rgb-state-climate-heat, var(--rgb-deep-orange)); - --rgb-state-climate-heat-cool: var(--mush-rgb-state-climate-heat-cool, var(--rgb-green)); - --rgb-state-climate-idle: var(--mush-rgb-state-climate-idle, var(--rgb-disabled)); - --rgb-state-climate-off: var(--mush-rgb-state-climate-off, var(--rgb-disabled)); + /* RGB */ + /* Standard colors */ + --rgb-red: var(--mush-rgb-red, var(--default-red)); + --rgb-pink: var(--mush-rgb-pink, var(--default-pink)); + --rgb-purple: var(--mush-rgb-purple, var(--default-purple)); + --rgb-deep-purple: var(--mush-rgb-deep-purple, var(--default-deep-purple)); + --rgb-indigo: var(--mush-rgb-indigo, var(--default-indigo)); + --rgb-blue: var(--mush-rgb-blue, var(--default-blue)); + --rgb-light-blue: var(--mush-rgb-light-blue, var(--default-light-blue)); + --rgb-cyan: var(--mush-rgb-cyan, var(--default-cyan)); + --rgb-teal: var(--mush-rgb-teal, var(--default-teal)); + --rgb-green: var(--mush-rgb-green, var(--default-green)); + --rgb-light-green: var(--mush-rgb-light-green, var(--default-light-green)); + --rgb-lime: var(--mush-rgb-lime, var(--default-lime)); + --rgb-yellow: var(--mush-rgb-yellow, var(--default-yellow)); + --rgb-amber: var(--mush-rgb-amber, var(--default-amber)); + --rgb-orange: var(--mush-rgb-orange, var(--default-orange)); + --rgb-deep-orange: var(--mush-rgb-deep-orange, var(--default-deep-orange)); + --rgb-brown: var(--mush-rgb-brown, var(--default-brown)); + --rgb-light-grey: var(--mush-rgb-light-grey, var(--default-light-grey)); + --rgb-grey: var(--mush-rgb-grey, var(--default-grey)); + --rgb-dark-grey: var(--mush-rgb-dark-grey, var(--default-dark-grey)); + --rgb-blue-grey: var(--mush-rgb-blue-grey, var(--default-blue-grey)); + --rgb-black: var(--mush-rgb-black, var(--default-black)); + --rgb-white: var(--mush-rgb-white, var(--default-white)); + --rgb-disabled: var(--mush-rgb-disabled, var(--default-disabled)); + + /* Action colors */ + --rgb-info: var(--mush-rgb-info, var(--rgb-blue)); + --rgb-success: var(--mush-rgb-success, var(--rgb-green)); + --rgb-warning: var(--mush-rgb-warning, var(--rgb-orange)); + --rgb-danger: var(--mush-rgb-danger, var(--rgb-red)); + + /* State colors */ + --rgb-state-vacuum: var(--mush-rgb-state-vacuum, var(--rgb-teal)); + --rgb-state-fan: var(--mush-rgb-state-fan, var(--rgb-green)); + --rgb-state-light: var(--mush-rgb-state-light, var(--rgb-orange)); + --rgb-state-entity: var(--mush-rgb-state-entity, var(--rgb-blue)); + --rgb-state-media-player: var( + --mush-rgb-state-media-player, + var(--rgb-indigo) + ); + --rgb-state-lock: var(--mush-rgb-state-lock, var(--rgb-blue)); + --rgb-state-number: var(--mush-rgb-state-number, var(--rgb-blue)); + --rgb-state-humidifier: var(--mush-rgb-state-humidifier, var(--rgb-purple)); + + /* State alarm colors */ + --rgb-state-alarm-disarmed: var( + --mush-rgb-state-alarm-disarmed, + var(--rgb-info) + ); + --rgb-state-alarm-armed: var( + --mush-rgb-state-alarm-armed, + var(--rgb-success) + ); + --rgb-state-alarm-triggered: var( + --mush-rgb-state-alarm-triggered, + var(--rgb-danger) + ); + + /* State person colors */ + --rgb-state-person-home: var( + --mush-rgb-state-person-home, + var(--rgb-success) + ); + --rgb-state-person-not-home: var( + --mush-rgb-state-person-not-home, + var(--rgb-danger) + ); + --rgb-state-person-zone: var(--mush-rgb-state-person-zone, var(--rgb-info)); + --rgb-state-person-unknown: var( + --mush-rgb-state-person-unknown, + var(--rgb-grey) + ); + + /* State update colors */ + --rgb-state-update-on: var(--mush-rgb-state-update-on, var(--rgb-orange)); + --rgb-state-update-off: var(--mush-rgb-update-off, var(--rgb-green)); + --rgb-state-update-installing: var( + --mush-rgb-update-installing, + var(--rgb-blue) + ); + + /* State lock colors */ + --rgb-state-lock-locked: var(--mush-rgb-state-lock-locked, var(--rgb-green)); + --rgb-state-lock-unlocked: var( + --mush-rgb-state-lock-unlocked, + var(--rgb-red) + ); + --rgb-state-lock-pending: var( + --mush-rgb-state-lock-pending, + var(--rgb-orange) + ); + + /* State cover colors */ + --rgb-state-cover-open: var(--mush-rgb-state-cover-open, var(--rgb-blue)); + --rgb-state-cover-closed: var( + --mush-rgb-state-cover-closed, + var(--rgb-disabled) + ); + + /* State climate colors */ + --rgb-state-climate-auto: var( + --mush-rgb-state-climate-auto, + var(--rgb-green) + ); + --rgb-state-climate-cool: var(--mush-rgb-state-climate-cool, var(--rgb-blue)); + --rgb-state-climate-dry: var(--mush-rgb-state-climate-dry, var(--rgb-orange)); + --rgb-state-climate-fan-only: var( + --mush-rgb-state-climate-fan-only, + var(--rgb-teal) + ); + --rgb-state-climate-heat: var( + --mush-rgb-state-climate-heat, + var(--rgb-deep-orange) + ); + --rgb-state-climate-heat-cool: var( + --mush-rgb-state-climate-heat-cool, + var(--rgb-green) + ); + --rgb-state-climate-idle: var( + --mush-rgb-state-climate-idle, + var(--rgb-disabled) + ); + --rgb-state-climate-off: var( + --mush-rgb-state-climate-off, + var(--rgb-disabled) + ); `; diff --git a/src/utils/weather.ts b/src/utils/weather.ts index bd26dfaa4..8a8617117 100644 --- a/src/utils/weather.ts +++ b/src/utils/weather.ts @@ -1,18 +1,18 @@ import { css, svg, SVGTemplateResult } from "lit"; const cloudyStates = new Set([ - "partlycloudy", - "cloudy", - "fog", - "windy", - "windy-variant", - "hail", - "rainy", - "snowy", - "snowy-rainy", - "pouring", - "lightning", - "lightning-rainy", + "partlycloudy", + "cloudy", + "fog", + "windy", + "windy-variant", + "hail", + "rainy", + "snowy", + "snowy-rainy", + "pouring", + "lightning", + "lightning-rainy", ]); const rainStates = new Set(["hail", "rainy", "pouring"]); @@ -24,68 +24,71 @@ const snowyStates = new Set(["snowy", "snowy-rainy"]); const lightningStates = new Set(["lightning", "lightning-rainy"]); export const weatherSVGStyles = css` - .rain { - fill: var(--weather-icon-rain-color, #30b3ff); - } - .sun { - fill: var(--weather-icon-sun-color, #fdd93c); - } - .moon { - fill: var(--weather-icon-moon-color, #fcf497); - } - .cloud-back { - fill: var(--weather-icon-cloud-back-color, #d4d4d4); - } - .cloud-front { - fill: var(--weather-icon-cloud-front-color, #f9f9f9); - } + .rain { + fill: var(--weather-icon-rain-color, #30b3ff); + } + .sun { + fill: var(--weather-icon-sun-color, #fdd93c); + } + .moon { + fill: var(--weather-icon-moon-color, #fcf497); + } + .cloud-back { + fill: var(--weather-icon-cloud-back-color, #d4d4d4); + } + .cloud-front { + fill: var(--weather-icon-cloud-front-color, #f9f9f9); + } `; -export const getWeatherStateSVG = (state: string, nightTime?: boolean): SVGTemplateResult => svg` +export const getWeatherStateSVG = ( + state: string, + nightTime?: boolean +): SVGTemplateResult => svg` ${ - state === "sunny" - ? svg` + state === "sunny" + ? svg` ` - : "" + : "" } ${ - state === "clear-night" - ? svg` + state === "clear-night" + ? svg` ` - : "" + : "" } ${ - state === "partlycloudy" && nightTime - ? svg` + state === "partlycloudy" && nightTime + ? svg` ` - : state === "partlycloudy" - ? svg` + : state === "partlycloudy" + ? svg` ` - : "" + : "" } ${ - cloudyStates.has(state) - ? svg` + cloudyStates.has(state) + ? svg` ` - : "" + : "" } ${ - rainStates.has(state) - ? svg` + rainStates.has(state) + ? svg` ` - : "" + : "" } ${ - state === "pouring" - ? svg` + state === "pouring" + ? svg` ` - : "" + : "" } ${ - windyStates.has(state) - ? svg` + windyStates.has(state) + ? svg` ` - : "" + : "" } ${ - snowyStates.has(state) - ? svg` + snowyStates.has(state) + ? svg` ` - : "" + : "" } ${ - lightningStates.has(state) - ? svg` + lightningStates.has(state) + ? svg` ` - : "" + : "" } `;