From 52e325749aafe78b60892c03f698fa8a876dfcfa Mon Sep 17 00:00:00 2001 From: benct Date: Fri, 10 Jan 2020 21:22:20 +0100 Subject: [PATCH] Refactor, improve and clean up code + new features - Replace Polymer with LitElement - Support HA Cast - Support custom button icons - Support hiding specific vacuum attributes (#27) - Vendor support for iRobot Roomba vacuums (#24) --- README.md | 3 +- xiaomi-vacuum-card.js | 518 +++++++++++++++++++++--------------------- 2 files changed, 266 insertions(+), 255 deletions(-) diff --git a/README.md b/README.md index 071b1cb..b3f2bac 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Using HACS, you might need to set the `image` config to something like `/local/c | image | string/bool | `/local/img/vacuum.png` | Custom path/name of background image (set to `false` to disable background) | buttons | object/bool | *(see below)* | Set to `false` to hide button row | labels | object/bool | *(see below)* | Set to `false` to hide details/labels +| icons | object | | Set custom button icons (same keys as `buttons` object) ### Buttons object @@ -66,7 +67,7 @@ If you think any more vendors should be added, feel free to open an issue or con | Name | Type | Default | Description | ---- | ---- | ------- | ----------- -| vendor | string | `xiaomi` | Supported vendors: `xiaomi`, `valetudo`, `ecovacs`, `deebot`, `robovac` +| vendor | string | `xiaomi` | Supported vendors: `xiaomi`, `valetudo`, `ecovacs`, `deebot`, `robovac`, `roomba` *Note: Vendor `ecovacs` and `robovac` shows the clean spot button instead of the stop button by default* diff --git a/xiaomi-vacuum-card.js b/xiaomi-vacuum-card.js index 876e77c..9f2442d 100644 --- a/xiaomi-vacuum-card.js +++ b/xiaomi-vacuum-card.js @@ -1,285 +1,295 @@ -class XiaomiVacuumCard extends Polymer.Element { +((LitElement) => { + const html = LitElement.prototype.html; + const css = LitElement.prototype.css; - static get template() { - return Polymer.html` - - - - - - - `; - } + ${this.state.showDetails ? html` +
+
${this.computeValue('main_brush')}
+
${this.computeValue('side_brush')}
+
${this.computeValue('filter')}
+
${this.computeValue('sensor')}
+
` : null} + ` : null} + ${this.state.showButtons ? html` +
+ ${Object.keys(this.state.buttons).map(this.renderButton.bind(this))} +
` : null} + `; + } - moreInfo() { this.fireEvent('hass-more-info'); } - startVaccum() { this.callService(this._config.service.start); } - pauseVacuum() { this.callService(this._config.service.pause); } - stopVacuum() { this.callService(this._config.service.stop); } - locateVacuum() { this.callService(this._config.service.locate); } - returnVacuum() { this.callService(this._config.service.return); } - cleanSpot() { this.callService(this._config.service.spot); } + renderButton(key) { + return this.state.buttons[key] + ? html`
` + : null; + } - callService(service) { - this._hass.callService('vacuum', service, {entity_id: this._config.entity}); - } + getValue(field, unit = '') { + const value = (this.stateObj && this.state.attributes[field] in this.stateObj.attributes) + ? this.stateObj.attributes[this.state.attributes[field]] + unit + : (this._hass ? this._hass.localize('state.default.unavailable') : 'Unavailable'); + return `${this.state.labels[field]}: ${value}`; + }; - fireEvent(type, options = {}) { - const event = new Event(type, { - bubbles: options.bubbles || true, - cancelable: options.cancelable || true, - composed: options.composed || true, - }); - event.detail = {entityId: this._config.entity}; - this.shadowRoot.dispatchEvent(event); - return event; - } + computeValue(field) { + if (this.state.attributes[field] === undefined || this.state.attributes[field] === false) { + return null; + } else if (this.stateObj && this.state.attributes[field] in this.stateObj.attributes) { + const value = this.stateObj.attributes[this.state.attributes[field]]; + return `${this.state.labels[field]}: ${this.state.computeValue(value)} ${this.state.labels.hours}`; + } else { + return `${this.state.labels[field]}: - `; + } + }; - getCardSize() { - if (this.name && this.showButtons) return 5; - if (this.name || this.showButtons) return 4; - return 3; - } + callService(service) { + this._hass.callService('vacuum', this.state.service[service], {entity_id: this.stateObj.entity_id}); + } - setConfig(config) { - const labels = { - status: 'Status', - battery: 'Battery', - mode: 'Mode', - main_brush: 'Main Brush', - side_brush: 'Side Brush', - filter: 'Filter', - sensor: 'Sensor', - hours: 'h', - }; + fireEvent(type, options = {}) { + const event = new Event(type, { + bubbles: options.bubbles || true, + cancelable: options.cancelable || true, + composed: options.composed || true, + }); + event.detail = {entityId: this.stateObj.entity_id}; + this.dispatchEvent(event); + } - const services = { - start: 'start', - pause: 'pause', - stop: 'stop', - locate: 'locate', - return: 'return_to_base', - spot: 'clean_spot', - }; + getCardSize() { + if (this.state.name && this.state.showButtons) return 5; + if (this.state.name || this.state.showButtons) return 4; + return 3; + } - const buttons = { - start: true, - pause: true, - stop: true, - spot: false, - locate: true, - return: true, - }; + setConfig(config) { + const labels = { + status: 'Status', + battery: 'Battery', + mode: 'Mode', + main_brush: 'Main Brush', + side_brush: 'Side Brush', + filter: 'Filter', + sensor: 'Sensor', + hours: 'h', + }; - const attributes = { - status: 'status', - battery: 'battery_level', - mode: 'fan_speed', - main_brush: 'main_brush_left', - side_brush: 'side_brush_left', - filter: 'filter_left', - sensor: 'sensor_dirty_left', - }; + const attributes = { + status: 'status', + battery: 'battery_level', + mode: 'fan_speed', + main_brush: 'main_brush_left', + side_brush: 'side_brush_left', + filter: 'filter_left', + sensor: 'sensor_dirty_left', + }; - const vendors = { - xiaomi: { - image: '/local/img/vacuum.png', - details: true, - }, - valetudo: { - image: '/local/img/vacuum.png', - details: true, - attributes: { - status: 'state', - main_brush: 'mainBrush', - side_brush: 'sideBrush', - filter: 'filter', - sensor: 'sensor', - }, - }, - robovac: { - image: '/local/img/vacuum.png', - details: false, - buttons: { - stop: false, - spot: true, + const services = { + start: 'start', + pause: 'pause', + stop: 'stop', + locate: 'locate', + return: 'return_to_base', + spot: 'clean_spot', + }; + + const buttons = { + start: true, + pause: true, + stop: true, + spot: false, + locate: true, + return: true, + }; + + const icons = { + start: 'mdi:play', + pause: 'mdi:pause', + stop: 'mdi:stop', + locate: 'mdi:map-marker', + return: 'mdi:home-map-marker', + spot: 'mdi:broom', + }; + + const vendors = { + xiaomi: { + details: true, }, - }, - ecovacs: { - image: '/local/img/vacuum_ecovacs.png', - details: false, - buttons: { - stop: false, - spot: true, + valetudo: { + details: true, + attributes: { + status: 'state', + main_brush: 'mainBrush', + side_brush: 'sideBrush', + filter: 'filter', + sensor: 'sensor', + }, }, - service: { - start: 'turn_on', - pause: 'stop', - stop: 'turn_off', + roomba: { + details: true, + attributes: { + main_brush: 'bin_present', + side_brush: 'bin_full', + filter: false, + sensor: false, + }, + labels: { + main_brush: 'Bin Present', + side_brush: 'Bin Full', + }, }, - }, - deebot: { - image: '/local/img/vacuum_ecovacs.png', - details: true, - service: { - start: 'turn_on', - pause: 'stop', - stop: 'turn_off', + robovac: { + details: false, + buttons: { + stop: false, + spot: true, + }, }, - attributes: { - main_brush: 'component_main_brush', - side_brush: 'component_side_brush', - filter: 'component_filter', + ecovacs: { + image: '/local/img/vacuum_ecovacs.png', + details: false, + buttons: { + stop: false, + spot: true, + }, + service: { + start: 'turn_on', + pause: 'stop', + stop: 'turn_off', + }, }, - computeValue: v => Math.round(Number(v) / 100), - } - }; + deebot: { + image: '/local/img/vacuum_ecovacs.png', + details: true, + service: { + start: 'turn_on', + pause: 'stop', + stop: 'turn_off', + }, + attributes: { + main_brush: 'component_main_brush', + side_brush: 'component_side_brush', + filter: 'component_filter', + sensor: false, + }, + computeValue: v => Math.round(Number(v) / 100), + } + }; - if (!config.entity) throw new Error('Please define an entity.'); - if (config.entity.split('.')[0] !== 'vacuum') throw new Error('Please define a vacuum entity.'); - if (config.vendor && !config.vendor in vendors) throw new Error('Please define a valid vendor.'); + if (!config.entity) throw new Error('Please define an entity.'); + if (config.entity.split('.')[0] !== 'vacuum') throw new Error('Please define a vacuum entity.'); + if (config.vendor && !config.vendor in vendors) throw new Error('Please define a valid vendor.'); - const vendor = vendors[config.vendor] || vendors.xiaomi; + const vendor = vendors[config.vendor] || vendors.xiaomi; - this.showDetails = vendor.details; - this.showButtons = config.buttons !== false; - this.showLabels = config.labels !== false; + this.state = { + showDetails: vendor.details, + showButtons: config.buttons !== false, + showLabels: config.labels !== false, + showName: config.name !== false, - config.service = Object.assign({}, services, vendor.service); - config.buttons = Object.assign({}, buttons, vendor.buttons, config.buttons); - config.attributes = Object.assign({}, attributes, vendor.attributes, config.attributes); - config.labels = Object.assign({}, labels, config.labels); + service: Object.assign({}, services, vendor.service), + buttons: Object.assign({}, buttons, vendor.buttons, config.buttons), + attributes: Object.assign({}, attributes, vendor.attributes, config.attributes), + labels: Object.assign({}, labels, vendor.labels, config.labels), + icons: Object.assign({}, icons, config.icons), + computeValue: vendor.computeValue || (val => val), + }; - this.getValue = (field, unit = '') => { - const value = (this.stateObj && config.attributes[field] in this.stateObj.attributes) - ? this.stateObj.attributes[config.attributes[field]] + unit - : (this._hass ? this._hass.localize('state.default.unavailable') : 'Unavailable'); - return `${config.labels[field]}: ${value}`; - }; + this.style = { + text: `color: ${config.image !== false ? 'white; text-shadow: 0 0 10px black;' : 'var(--primary-text-color);'}`, + content: `padding: ${config.showButtons ? '16px 16px 4px' : '16px'};`, + background: config.image !== false ? `background-image: url('${config.image || vendor.image || '/local/img/vacuum.png'}')` : '' + }; - this.computeValue = field => { - if (this.stateObj && config.attributes[field] in this.stateObj.attributes) { - const value = this.stateObj.attributes[config.attributes[field]]; - return `${config.labels[field]}: ${vendor.computeValue ? vendor.computeValue(value) : value} ${config.labels.hours}`; - } else { - return `${config.labels[field]}: - `; - } - }; - - this.contentText = `color: ${config.image !== false ? 'white; text-shadow: 0 0 10px black;' : 'var(--primary-text-color)'}`; - this.contentStyle = `padding: ${this.showButtons ? '16px 16px 4px' : '16px'}; ${this.contentText}`; - this.backgroundImage = config.image !== false ? `background-image: url('${config.image || vendor.image}')` : ''; - - this._config = config; - } + this._config = config; + } - set hass(hass) { - this._hass = hass; + set hass(hass) { + this._hass = hass; - if (hass && this._config) { - this.stateObj = this._config.entity in hass.states ? hass.states[this._config.entity] : null; + if (hass && this._config) { + this.stateObj = this._config.entity in hass.states ? hass.states[this._config.entity] : null; - if (this.stateObj) { - this.name = this._config.name !== false && (this._config.name || this.stateObj.attributes.friendly_name); + if (this.stateObj && this.state.showName) { + this.state.name = this._config.name || this.stateObj.attributes.friendly_name; + } } } } -} -customElements.define('xiaomi-vacuum-card', XiaomiVacuumCard); + customElements.define('xiaomi-vacuum-card', XiaomiVacuumCard); +})(window.LitElement || Object.getPrototypeOf(customElements.get("hui-view"))); \ No newline at end of file