diff --git a/src/elements/lc-config-card.js b/src/elements/lc-config-card.js new file mode 100644 index 0000000..752f1f0 --- /dev/null +++ b/src/elements/lc-config-card.js @@ -0,0 +1,454 @@ +import { LitElement, html, css, nothing } from 'lit'; +import { hasConfigOrEntityChanged, } from 'custom-card-helpers'; + +class LandroidConfigCard extends LitElement { + static properties = { + hass: { type: Object }, + config: { type: Object }, + deviceName: { type: String }, + }; + + /** + * Lifecycle method to handle the component being connected to the DOM. + * + * This method is called when the component is connected to the DOM. It is the + * opposite of `disconnectedCallback`, and is called when the component is + * added to the DOM. This is a good place to set up any initial state or + * perform any setup that needs to happen only once. + * + * @return {void} This function does not return anything. + */ + connectedCallback() { + super.connectedCallback(); + } + + /** + * Lifecycle method to handle the component being disconnected from the DOM. + * + * This method is called when the component is disconnected from the DOM. It + * is the opposite of `connectedCallback`, and is called when the component is + * removed from the DOM. + * + * @see https://lit.dev/docs/components/lifecycle/#disconnectedcallback + */ + disconnectedCallback() { + super.disconnectedCallback(); + } + + /** + * Lifecycle method that is called after the component has been rendered for the first time. + * + * This method sets the `_firstRendered` property to `true`, indicating that the component + * has completed its first render. It can be used to perform any setup or initialization + * that depends on the component being fully rendered. + * + * @return {void} This function does not return anything. + */ + firstUpdated() { + this._firstRendered = true; + } + + /** + * Lifecycle method to determine if the component should update when its + * properties change. + * + * This method will return true if the component's configuration or any of its + * entities have changed. Otherwise, it will return false. + * + * @param {Object} changedProps - An object with information about which + * properties have changed. + * @return {boolean} True if the component should update, false otherwise. + */ + shouldUpdate(changedProps) { + return hasConfigOrEntityChanged(this, changedProps); + } + + /** + * Styles for the component. + * + * The styles are scoped to the component and are used to style the + * component's host element. The styles are defined using LitElement's + * `css` tag function. + * + * @return {CSSResult} The styles for the component. + */ + static get styles() { + return css` + :host { + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: var(--lc-spacing); + border-top: 1px solid var(--lc-divider-color); + border-bottom: 1px solid var(--lc-divider-color); + } + + /* hui-entities-card */ + #states { + flex: 1 1 0%; + } + + #states > * { + margin: 8px 0px; + } + + #states > :first-child { + margin-top: 0px; + } + + #states > *:last-child { + margin-bottom: 0; + } + + #states > div > * { + overflow: clip visible; + } + + #states > div { + position: relative; + } + + .icon { + padding: 0px 18px 0px 8px; + } + + /* hui-input-number-entity-row */ + .flex { + display: flex; + align-items: center; + justify-content: flex-end; + flex-grow: 2; + } + + .state { + min-width: 45px; + text-align: end; + } + + .slider { + flex-grow: 2; + width: 100px; + max-width: 200px; + } + + ha-textfield { + text-align: end; + } + + ha-slider { + width: 100%; + max-width: 200px; + } + + /* hui-input-select-entity-row */ + ha-select { + width: 100%; + --ha-select-min-width: 0; + } + `; + } + + /** + * Renders the configuration card UI based on the provided entities. + * + * This function checks if the configuration is valid and iterates over the + * entities to render corresponding components based on their domain type, + * such as button, number, select, or switch. If the entity's domain is not + * recognized, it returns nothing. The rendered entities are wrapped in a + * container with the class "entitiescard". + * + * @return {TemplateResult|nothing} The rendered configuration card UI or + * nothing if the configuration or entities are not provided. + */ + render() { + if (!this.config || !this.config.entities) return nothing; + + const entities = this.config.entities.map((entityId) => { + const domain = entityId.split('.')[0]; + + switch (domain) { + case 'button': + return this.renderButtonEntity(entityId); + case 'number': + return this.renderNumberEntity(entityId); + case 'select': + return this.renderSelectEntity(entityId); + case 'switch': + return this.renderToggleSwitchEntity(entityId); + default: + return nothing; + } + }); + + return html` +
+
+ ${entities} +
+
+ `; + } + +/** + * Renders a button entity row for the provided entity ID. + * + * This function retrieves the state object for the given entity ID from Home Assistant, + * checks if the entity is available, and constructs a configuration object containing + * the entity ID, a friendly name (excluding the device name), and an icon for display. + * It returns a lit-html template rendering a generic entity row with a button that, + * when clicked, triggers the `pressButton` method for the entity. The button is disabled + * if the entity is unavailable. + * + * @param {string} entityId - The ID of the entity to be rendered. + * @return {TemplateResult|nothing} A lit-html template rendering the button entity row, + * or `nothing` if the entity is unavailable. + */ + renderButtonEntity(entityId) { + const stateObj = this.hass.states[entityId]; + if (!stateObj || stateObj.state === 'unavailable') return nothing; + + const config = { + entity: entityId, + name: stateObj.attributes.friendly_name.replace(`${this.deviceName} `, ''), + icon: stateObj.attributes.icon, + }; + + return html` + + this.pressButton(e, entityId)} + .disabled=${stateObj.state === 'unavailable'} + > + ${this.hass.localize('ui.card.button.press')} + + + `; + } + + /** + * Renders a number entity row for the provided entity ID. + * + * This function retrieves the state object for the given entity ID from Home Assistant, + * checks if the entity is available, and constructs a configuration object containing + * the entity ID, a friendly name (excluding the device name), and an icon for display. + * It returns a lit-html template rendering a generic entity row with either a slider + * (if the entity is a slider or an auto entity with 256 or fewer steps) or a textfield + * (otherwise). The slider or textfield is disabled if the entity is unavailable. The + * value of the slider or textfield is the state of the entity, and the unit of measurement + * is used as the suffix. When the slider or textfield value changes, the method + * `numberValueChanged` is called with the event and the state object as arguments. + * + * @param {string} entityId - The ID of the entity to be rendered. + * @return {TemplateResult|nothing} A lit-html template rendering the number entity row, + * or `nothing` if the entity is unavailable. + */ + renderNumberEntity(entityId) { + const stateObj = this.hass.states[entityId]; + if (!stateObj || stateObj.state === 'unavailable') return nothing; + + const config = { + entity: entityId, + name: stateObj.attributes.friendly_name.replace(`${this.deviceName} `, ''), + icon: stateObj.attributes.icon, + }; + + return html` + + ${stateObj.attributes.mode === 'slider' || + (stateObj.attributes.mode === 'auto' && + (Number(stateObj.attributes.max) - Number(stateObj.attributes.min)) / + Number(stateObj.attributes.step) <= + 256) + ? html` + this.numberValueChanged(e, stateObj)} + > + + ${this.hass.formatEntityState(stateObj)} + + ` + : html` +
+ this.numberValueChanged(e, stateObj)} + > +
+ `} +
+ `; + } + + /** + * Renders a select entity element for the given entityId. + * + * @param {string} entityId - The entityId to render a select element for. + * @return {TemplateResult} The rendered element as a TemplateResult. + */ + renderSelectEntity(entityId) { + const stateObj = this.hass.states[entityId]; + if (!stateObj || stateObj.state === 'unavailable') return nothing; + + const config = { + entity: entityId, + name: stateObj.attributes.friendly_name.replace(`${this.deviceName} `, ''), + icon: stateObj.attributes.icon, + }; + + return html` + + this.selectedChanged(e, stateObj)} + @closed=${(e) => e.stopPropagation()} + @click=${(e) => e.stopPropagation()} + > + ${stateObj.attributes.options + ? stateObj.attributes.options.map( + (option) => html` + + ${this.hass.formatEntityState(stateObj, option)} + + `, + ) + : ''} + + + `; + // ${this.hass.localize(`ui.components.entity.entity-picker.options.${option}`)} + } + + /** + * Renders a toggle switch for the given entity ID. + * + * @param {string} entityId - The ID of the entity to be rendered. + * @return {TemplateResult} The rendered toggle switch entity row, or `nothing` if the entity is unavailable. + */ + renderToggleSwitchEntity(entityId) { + const stateObj = this.hass.states[entityId]; + if (!stateObj || stateObj.state === 'unavailable') return nothing; + + const config = { + entity: entityId, + name: stateObj.attributes.friendly_name.replace(`${this.deviceName} `, ''), + icon: stateObj.attributes.icon, + }; + + return html` + + this.toggleChanged(e, stateObj)} + > + + `; + } + + /** + * Triggers a press action for the specified button entity. + * + * This function stops the event propagation and calls the Home Assistant + * service to perform a press action on the button entity identified by + * the provided entity ID. + * + * @param {Event} e - The event object associated with the button press. + * @param {string} entity_id - The ID of the button entity to be pressed. + */ + pressButton(e, entity_id) { + e.stopPropagation(); + this.hass.callService("button", "press", { + entity_id, + }); + } + + /** + * Handles a change in the value of a number input element + * + * This function is called when the user changes the value of a number input + * element associated with a number entity. It checks if the new value is + * different from the current state of the entity, and if so, calls the Home + * Assistant service to update the state of the entity. + * + * @param {Event} e - The event object associated with the input change. + * @param {Object} stateObj - The entity object with the state to be updated. + */ + numberValueChanged(e, stateObj) { + if (e.target.value !== stateObj.state) { + this.hass.callService('number', 'set_value', { + entity_id: stateObj.entity_id, + value: e.target.value, + }); + } + } + + /** + * Handles a change in the value of a select input element + * + * This function is called when the user changes the value of a select input + * element associated with a select entity. It checks if the new value is + * different from the current state of the entity and if the new value is + * included in the list of options for the entity. If the new value is valid, + * it calls the Home Assistant service to update the state of the entity. + * + * @param {Event} e - The event object associated with the input change. + * @param {Object} stateObj - The entity object with the state to be updated. + */ + selectedChanged(e, stateObj) { + const option = e.target.value; + if ( + option === stateObj.state || + !stateObj.attributes.options.includes(option) + ) { + return; + } + + this.hass.callService('select', 'select_option', { + entity_id: [stateObj.entity_id], + option, + }); + } + + /** + * Handles a change in the value of a toggle input element + * + * This function is called when the user changes the value of a toggle input + * element associated with a toggle entity. It checks if the new value is + * different from the current state of the entity, and if so, it calls the + * Home Assistant service to update the state of the entity. + * + * @param {Event} e - The event object associated with the input change. + * @param {Object} stateObj - The entity object with the state to be updated. + */ + toggleChanged(e, stateObj) { + const newState = e.target.checked; + if (newState === stateObj.state) return; + this.hass.callService('switch', newState ? 'turn_on' : 'turn_off', { + entity_id: [stateObj.entity_id], + }).then(() => { + this.requestUpdate(); + }); + } + +} + +customElements.define('lc-config-card', LandroidConfigCard); diff --git a/src/landroid-card.js b/src/landroid-card.js index 56c2995..1dd4a72 100644 --- a/src/landroid-card.js +++ b/src/landroid-card.js @@ -11,7 +11,7 @@ import styles from './styles'; import defaultImage from './landroid.svg'; import { version } from '../package.json'; import './landroid-card-editor'; -import { stopPropagation, isObject, wifiStrenghtToQuality } from './helpers'; +import { isObject, wifiStrenghtToQuality } from './helpers'; import * as consts from './constants'; import { DEFAULT_LANG, defaultConfig } from './defaults'; import LandroidCardEditor from './landroid-card-editor'; @@ -804,270 +804,6 @@ return html` `; } - /** - * Renders the configuration card for the entities specified in the config. - * - * @return {TemplateResult} The rendered configuration card as a lit-html TemplateResult or nothing. - */ - renderConfigCard() { - if (!this.config || !this.config.settings || !this.showConfigCard) return nothing; - - const entities = this.config.settings.map((entityId) => { - const domain = entityId.split('.')[0]; - - switch (domain) { - case 'button': - return this.renderButtonEntity(entityId); - case 'number': - return this.renderNumber(entityId); - case 'select': - return this.renderSelectRow(entityId); - case 'switch': - return this.renderToggleSwitchEntity(entityId); - default: - return nothing; - } - }); - - return html` -
-
- ${entities} -
-
- `; -} - - /** - * Presses a button entity when the corresponding button is clicked. - * - * @param {Event} e - The click event. - * @param {string} entity_id - The entity ID of the button entity. - */ - pressButton(e, entity_id) { - e.stopPropagation(); - this.hass.callService("button", "press", { - entity_id, - }); - } - - /** - * Renders a button for a given entity in the UI. - * - * @param {Object} stateObj - The entity object to render. - * @return {TemplateResult} The rendered button as a TemplateResult. - */ - renderButtonEntity(entityId) { - const stateObj = this.getEntityObject(entityId); - if (!stateObj || stateObj.state === consts.UNAVAILABLE) return nothing; - - const config = { - entity: entityId, - name: this.getEntityName(stateObj), - icon: stateObj.attributes.icon, - }; - - return html` - - this.pressButton(e, config.entity)} - .disabled=${stateObj.state === consts.UNAVAILABLE} - > - ${this.hass.localize("ui.card.button.press")} - - - `; - } - - /** - * Renders a number input row (Slider or TextField) for an entity card. - * - * @param {Object} stateObj - The entity object. - * @return {TemplateResult} The rendered number input row. - */ - renderNumber(entityId) { - const stateObj = this.getEntityObject(entityId); - if (!stateObj || stateObj.state === consts.UNAVAILABLE) return nothing; - - const config = { - entity: entityId, - name: this.getEntityName(stateObj), - icon: stateObj.attributes.icon, - }; - - return html` - - ${stateObj.attributes.mode === 'slider' || - (stateObj.attributes.mode === 'auto' && - (Number(stateObj.attributes.max) - Number(stateObj.attributes.min)) / - Number(stateObj.attributes.step) <= - 256) - ? html` -
- this.numberValueChanged(e, stateObj)} - > - - ${this.hass.formatEntityState(stateObj)} - -
- ` - : html` -
- this.numberValueChanged(e, stateObj)} - > -
- `} -
- `; - } - - /** - * Handles the change event when a number input value is changed. - * If the new value is different from the current state of the entity, - * it calls the 'number.set_value' service with the updated value. - * - * @param {Event} e - The event object representing the change event. - * @param {Object} stateObj - The entity object representing the state. - * @return {void} This function does not return anything. - */ - numberValueChanged(e, stateObj) { - if (e.target.value !== stateObj.state) { - // this.callService(e, 'number.set_value'); - this.hass.callService('number', 'set_value', { - entity_id: stateObj.entity_id, - value: e.target.value, - }); - } - } - - /** - * Renders a select row for the given entity state object. - * - * @param {Object} stateObj - The entity state object. - * @return {TemplateResult} The rendered select row. - */ - renderSelectRow(entityId) { - const stateObj = this.getEntityObject(entityId); - if (!stateObj || stateObj.state === consts.UNAVAILABLE) return nothing; - - const config = { - entity: entityId, - name: this.getEntityName(stateObj), - icon: stateObj.attributes.icon, - }; - - return html` - - this.selectedChanged(e, stateObj)} - @click=${stopPropagation} - @closed=${stopPropagation} - > - ${stateObj.attributes.options - ? stateObj.attributes.options.map( - (option) => html` - - ${this.hass.formatEntityState(stateObj, option)} - - `, - ) - : ''} - - - `; - } - - /** - * Handles the change event when a select option is selected. - * - * @param {Event} e - The event object representing the change event. - * @param {Object} stateObj - The entity object representing the state. - * @return {void} This function does not return anything. - */ - selectedChanged(e, stateObj) { - const option = e.target.value; - if ( - option === stateObj.state || - !stateObj.attributes.options.includes(option) - ) { - return; - } - - this.hass.callService('select', 'select_option', { - entity_id: [stateObj.entity_id], - option, - }); - } - - /** - * Generates Toggle Switch Entity Row for Entities Card - * @param {Object} stateObj Entity object - * @return {TemplateResult} Toggle Switch Entity Row - */ - renderToggleSwitchEntity(entityId) { - const stateObj = this.getEntityObject(entityId); - if (!stateObj || stateObj.state === consts.UNAVAILABLE) return nothing; - - const config = { - entity: entityId, - name: this.getEntityName(stateObj), - icon: stateObj.attributes.icon, - }; - - return html` - - this.toggleChanged(e, stateObj)} - > - - `; - } - - /** - * Handles the change event when a switch is toggled. - * - * @param {Event} e - The event object representing the change event. - * @param {Object} stateObj - The entity object representing the state. - * @return {void} This function does not return anything. - */ - toggleChanged(e, stateObj) { - const newState = e.target.checked; - if (newState === stateObj.state) return; - this.callService(e, `switch.${newState ? 'turn_on' : 'turn_off'}`, { - entity_id: stateObj.entity_id, - // isRequest: true, - }); - // this.hass.callService('switch', newState ? 'turn_on' : 'turn_off', { - // entity_id: [stateObj.entity_id], - // }).then(() => { - // this.requestUpdate(); - // }); - } - /** * Renders the Entities Card for a given card type. * @@ -1131,6 +867,55 @@ return html` `; } + /** + * Renders the toolbar component based on the current state. + * + * @param {string} state - The current state of the component. + * @return {TemplateResult} The rendered toolbar component. + */ + renderToolbar(state) { + if (!this.showToolbar) { + return nothing; + } + + const dailyProgress = this.getEntityObject( + consts.SENSOR_DAILY_PROGRESS_SUFFIX, + ); + + return html` +
+ ${this.renderButtonsForState(state)} +
+ ${this.renderShortcuts()} + ${this.config.settings + ? html` + + + + ` + : nothing + } + ${dailyProgress + ? html` + + ` + : nothing} +
+ `; + } + /** * Renders the buttons based on the state of the lawn mower. * @@ -1196,55 +981,24 @@ return html` } /** - * Renders the toolbar component based on the current state. - * - * @param {string} state - The current state of the component. - * @return {TemplateResult} The rendered toolbar component. - */ - renderToolbar(state) { - if (!this.showToolbar) { - return nothing; - } - - const dailyProgress = this.getEntityObject( - consts.SENSOR_DAILY_PROGRESS_SUFFIX, - ); + * Renders a configuration card to edit the settings. + * + * The `lc-config-card` element is created and its properties are set based on the + * component's configuration and device name. + * + * @return {HTMLElement} The rendered configuration card element. + */ + renderConfigCard() { + if (!this.config || !this.config.settings || !this.showConfigCard) return nothing; - return html` -
- ${this.renderButtonsForState(state)} -
- ${this.renderShortcuts()} - ${this.config.settings - ? html` - - - - ` - : nothing - } - ${dailyProgress - ? html` - - ` - : nothing} -
- `; + const configCard = document.createElement('lc-config-card'); + configCard.hass = this.hass; + configCard.config = { entities: this.config.settings, }; + configCard.deviceName = this.getAttributes().friendly_name; + return configCard; } - /** + /** * Renders the HTML template for the component. * * @return {TemplateResult} The rendered HTML template.