Total overhaul!

This commit is contained in:
Thomas Lovén 2019-06-13 22:45:59 +02:00
parent 46be70ee49
commit 1d29e2365f
14 changed files with 611 additions and 436 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
slider-entity-row.js binary

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
package-lock.json
package.json
webpack.config.js

42
Makefile Normal file
View File

@ -0,0 +1,42 @@
AUTHOR := Thomas Lovén
CARD_TOOLS := $(PWD)/../card-tools
PACKAGE := $(shell basename $(CURDIR))
PACKAGE := $(PACKAGE:lovelace-%=%)
DOCKER_CMD:=docker run --rm -v $(CARD_TOOLS):/card-tools:ro -v $(PWD):/usr/src/$(PACKAGE) -w="/usr/src/$(PACKAGE)" node:11
build: setup
$(DOCKER_CMD) npm run build
dev: setup
$(DOCKER_CMD) npm run watch
setup: package.json package-lock.json webpack.config.js
clean:
rm package.json package-lock.json webpack.config.js
rm -r node_modules
rm $(PACKAGE).js
define WEBPACK_CONFIG
const path = require('path');
module.exports = {
entry: './src/main.js',
mode: 'production',
output: {
filename: '$(PACKAGE).js',
path: path.resolve(__dirname)
}
};
endef
export WEBPACK_CONFIG
webpack.config.js:
echo "$$WEBPACK_CONFIG" >> $@
package-lock.json:
$(DOCKER_CMD) npm install webpack webpack-cli --save-dev
package.json:
$(DOCKER_CMD) /bin/bash -c "npm set init.license 'MIT' && npm set init.author.name '$(AUTHOR)' && npm init -y"
$(DOCKER_CMD) sed -E -i -e '/^ +"main"/d' -e '/^ +"scripts"/a\ "build": "webpack",' -e '/^ +"scripts"/a\ "watch": "webpack --watch --mode=development",' -e '2a\ "private": true,' $@

129
README.md
View File

@ -1,105 +1,80 @@
slider-entity-row slider-entity-row
================= =================
Add a slider to rows in lovelace entity cards Add a slider to rows in lovelace [entities](https://www.home-assistant.io/lovelace/entities/) cards.
This works for: For installation instructions [see this guide](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins).
Install `slider-entity-row.js` as a `module`.
## Usage
Add this to an entities card:
```yaml
type: entities
entities:
- light.bed_light
- type: custom:slider-entity-row
entity: light.kitchen_lights
```
![slider-entity-row](https://user-images.githubusercontent.com/1299821/59467898-15b16600-8e31-11e9-9924-53b108572d3a.png)
Currenly supported entity domains:
- `light` - set brightness - `light` - set brightness
- `media_player` - set volume - `media_player` - set volume
- `climate` - set temperature - `climate` - set temperature
- `cover` - set position - `cover` - set position
- `fan` - set speed (assumes first setting is `off`) - `fan` - set speed (assumes first setting is `off`)
- `input_number` - `input_number` - set value (only if `mode: slider`)
- `input_select` - `input_select` - select option
![slider-entity-row](https://user-images.githubusercontent.com/1299821/48869222-b6303200-eddc-11e8-8b8c-7b4a9601df7a.png) ![domains](https://user-images.githubusercontent.com/1299821/59467899-1813c000-8e31-11e9-8abd-34c887a7db2a.png)
### Options
- `toggle: true` - Show a toggle instead of current state
- `hide_state: true` - Do not display current state
- `hide_when_off: true` - Hide the slider when state is `off`
- `full_row: true` - Hide icon and name and stretch slider to full width
- `min: <value>` - Set minimum value of slider
- `max: <value>` - Set maximum value of slider
- `step: <value>` - Set step size of slider
```yaml ```yaml
- title: slider-entity-row type: entities
cards: title: Options
- type: entities
title: Domains
show_header_toggle: false
entities: entities:
- input_number.slider
- type: section
label: light
- type: custom:slider-entity-row - type: custom:slider-entity-row
entity: light.bed_light entity: light.bed_light
name: Default
- type: custom:slider-entity-row
entity: light.bed_light
name: toggle
toggle: true
- type: custom:slider-entity-row
entity: light.bed_light
name: hide_state
hide_state: true
- type: custom:slider-entity-row - type: custom:slider-entity-row
entity: light.ceiling_lights entity: light.ceiling_lights
name: hide_when_off
hide_when_off: true
- type: custom:slider-entity-row - type: custom:slider-entity-row
entity: light.kitchen_lights entity: light.ceiling_lights
name: hide_when_off + toggle
- type: section hide_when_off: true
label: media_player
- type: custom:slider-entity-row
entity: media_player.bedroom
- type: custom:slider-entity-row
entity: media_player.living_room
- type: custom:slider-entity-row
entity: media_player.lounge_room
- type: custom:slider-entity-row
entity: media_player.walkman
- type: section
label: cover
- type: custom:slider-entity-row
entity: cover.hall_window
- type: custom:slider-entity-row
entity: cover.garage_door
- type: custom:slider-entity-row
entity: cover.living_room_window
- type: entities
title: Options
show_header_toggle: false
entities:
- type: section
label: default
- type: custom:slider-entity-row
entity: light.bed_light
- type: custom:slider-entity-row
entity: media_player.bedroom
- type: custom:slider-entity-row
entity: cover.hall_window
- type: section
label: "toggle: true"
- type: custom:slider-entity-row
entity: light.bed_light
toggle: true toggle: true
- type: section - type: section
label: "full_row: true" label: full_row
- entity: light.bed_light
- type: custom:slider-entity-row - type: custom:slider-entity-row
entity: light.bed_light entity: light.bed_light
full_row: true name: hide_state
- entity: media_player.bedroom
- type: custom:slider-entity-row
entity: media_player.bedroom
full_row: true
- entity: cover.hall_window
- type: custom:slider-entity-row
entity: cover.hall_window
full_row: true full_row: true
``` ```
### Extra options ![options](https://user-images.githubusercontent.com/1299821/59467902-19dd8380-8e31-11e9-9173-97c9b6be3179.png)
`hide_state` - (default: false) Set to true to hide the percentage display.
`min` - (default: 0) Minimum value of slider
`max` - (default: 100) Maximum value of slider
`step` - (default: 5) Step size of slider
Note that slider values are in percent and will be rescaled e.g. for lights which require a brightness setting between 0 and 255.
`hide_when_off` - Hide the slider when entity is off.
--- ---
Thanks to Gabe Cook (@gabe565) for help with fan and input_select support. <a href="https://www.buymeacoffee.com/uqD6KHCdJ" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/white_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a>

File diff suppressed because one or more lines are too long

35
src/climate-controller.js Normal file
View File

@ -0,0 +1,35 @@
import {Controller} from "./controller.js";
export class ClimateController extends Controller {
get _value() {
return this.stateObj.attributes.temperature;
}
set _value(value) {
this._hass.callService("climate", "set_temperature", {
entity_id: this.stateObj.entity_id,
temperature: value,
});
}
get string() {
if (this.stateObj.attributes.operation_mode === "off")
return this._hass.localize("state.climate.off");
return `${this.value} ${this._hass.config.unit_system.temperature}`;
}
get isOff() {
return this.stateObj.attributes.operation_mode === "off";
}
get _min() {
return this.stateObj.attributes.min_temp;
}
get _max() {
return this.stateObj.attributes.max_temp;
}
get _step() {
return 1;
}
}

59
src/controller.js Normal file
View File

@ -0,0 +1,59 @@
export class Controller {
constructor(config) {
this._config = config;
}
set hass(hass) {
this._hass = hass;
this.stateObj = this._config.entity in hass.states ? hass.states[this._config.entity] : null;
}
get value() {
if(this._value)
return Math.round(this._value/this.step)*this.step;
return 0;
}
set value(value) {
if (value !== this.value)
this._value = value;
}
get string() {
return `${this.value}`;
}
get hidden() {
return false;
}
get hasSlider() {
return true;
}
get hasToggle() {
return true;
}
get isOff() {
return this.value === 0;
}
get min() {
if (this._config.min !== undefined)
return this._config.min;
if (this._min !== undefined)
return this._min;
return 0;
}
get max() {
if (this._config.max !== undefined)
return this._config.max;
if (this._max !== undefined)
return this._max;
return 100;
}
get step() {
if (this._config.step !== undefined)
return this._config.step;
if (this._step !== undefined)
return this._step;
return 5;
}
}

40
src/cover-controller.js Normal file
View File

@ -0,0 +1,40 @@
import {Controller} from "./controller.js";
export class CoverController extends Controller {
get _value() {
return this.stateObj.state === "open"
? this.stateObj.attributes.current_position
: 0;
}
set _value(value) {
this._hass.callService("cover", "set_cover_position", {
entity_id: this.stateObj.entity_id,
position: value,
});
}
get string() {
if (!this.hasSlider)
return "";
if (this.stateObj.state === "closed")
return this._hass.localize("state.cover.closed");
return `${this.value} %`
}
get hasToggle() {
return false;
}
get hasSlider() {
if ("current_position" in this.stateObj.attributes) return true;
if (("supported_features" in this.stateObj.attributes) &&
(this.stateObj.attributes.supported_features & 4)) return true;
return false;
}
get _step() {
return 10;
}
}

43
src/fan-controller.js Normal file
View File

@ -0,0 +1,43 @@
import {Controller} from "./controller.js";
export class FanController extends Controller {
get _value() {
return (this.stateObj.state !== "off")
? this.stateObj.attributes.speed_list.indexOf(this.stateObj.attributes.speed)
: 0;
}
set _value(value) {
if (value in this.stateObj.attributes.speed_list) {
this._hass.callService("fan", "turn_on", {
entity_id: this.stateObj.entity_id,
speed: this.stateObj.attributes.speed_list[value],
});
} else {
this._hass.callService("fan", "turn_off", {
entity_id: this.stateObj.entity_id,
});
}
}
get string() {
if (this.stateObj.state === "off")
return this._hass.localize("state.default.off");
return this.stateObj.attributes.speed;
}
get hasSlider() {
if ("speed" in this.stateObj.attributes) return true;
return false;
}
get _max() {
return this.stateObj.attributes.speed_list.length -1;
}
get _step() {
return 1;
}
}

View File

@ -0,0 +1,44 @@
import {Controller} from "./controller.js";
export class InputNumberController extends Controller {
get _value() {
return this.stateObj.state;
}
set _value(value) {
this._hass.callService("input_number", "set_value", {
entity_id: this.stateObj.entity_id,
value: value,
});
}
get string() {
return `${Math.round(this.stateObj.state)}`
}
get isOff() {
return false;
}
get hasToggle() {
return false;
}
get hasSlider() {
return this.stateObj.attributes.mode === "slider";
}
get _min() {
return this.stateObj.attributes.min;
}
get _max() {
return this.stateObj.attributes.max;
}
get _step() {
return this.stateObj.attributes.step;
}
}

View File

@ -0,0 +1,41 @@
import {Controller} from "./controller.js";
export class InputSelectController extends Controller {
get _value() {
return this.stateObj.attributes.options.indexOf(this.stateObj.state);
}
set _value(value) {
if (value in this.stateObj.attributes.options)
this._hass.callService("input_select", "select_option", {
entity_id: this.stateObj.entity_id,
option: this.stateObj.attributes.options[value],
});
}
get string() {
return this.stateObj.state;
}
get isOff() {
return false;
}
get hasToggle() {
return false;
}
get hasSlider() {
return this.stateObj.attributes.options && this.stateObj.attributes.options.length > 0
}
get _max() {
return this.stateObj.attributes.options.length - 1;
}
get _step() {
return 1;
}
}

37
src/light-controller.js Normal file
View File

@ -0,0 +1,37 @@
import {Controller} from "./controller.js";
export class LightController extends Controller {
get _value() {
return (this.stateObj.state === "on")
? Math.ceil(this.stateObj.attributes.brightness*100.0/255)
: 0;
}
set _value(value) {
value = Math.ceil(value/100.0*255);
if (value) {
this._hass.callService("light", "turn_on", {
entity_id: this.stateObj.entity_id,
brightness: value,
});
} else {
this._hass.callService("light", "turn_off", {
entity_id: this.stateObj.entity_id,
});
}
}
get string() {
if (this.stateObj.state === "off")
return this._hass.localize("state.default.off");
return `${this.value} %`;
}
get hasSlider() {
if ("brightness" in this.stateObj.attributes) return true;
if (("supported_features" in this.stateObj.attributes) &&
(this.stateObj.attributes.supported_features & 1)) return true;
return false;
}
}

119
src/main.js Normal file
View File

@ -0,0 +1,119 @@
import {LitElement, html, css} from "/card-tools/lit-element.js";
import { Controller } from "./controller.js";
import { LightController } from "./light-controller.js";
import { MediaPlayerController } from "./media-player-controller.js";
import { ClimateController } from "./climate-controller.js";
import { CoverController } from "./cover-controller.js";
import { FanController } from "./fan-controller.js";
import { InputNumberController } from "./input-number-controller.js";
import { InputSelectController } from "./input-select-controller.js";
const controllers = {
light: LightController,
media_player: MediaPlayerController,
climate: ClimateController,
cover: CoverController,
fan: FanController,
input_number: InputNumberController,
input_select: InputSelectController,
};
class SliderEntityRow extends LitElement {
static get properties() {
return {
hass: {},
};
}
setConfig(config) {
this._config = config;
const domain = config.entity.split('.')[0];
const ctrlClass = controllers[domain];
if(!ctrlClass)
throw new Error(`Unsupported entity type: ${domain}`);
this.ctrl = new ctrlClass(config);
}
render() {
const c = this.ctrl;
c.hass = this.hass;
const slider = html`
<ha-slider
.min=${c.min}
.max=${c.max}
.step=${c.step}
.value=${c.value}
pin
@change=${(ev) => c.value = this.shadowRoot.querySelector("ha-slider").value}
class=${this._config.full_row ? "full" : ""}
></ha-slider>
`;
const toggle = html`
<ha-entity-toggle
.stateObj=${this.hass.states[this._config.entity]}
.hass=${this.hass}
></ha-entity-toggle>
`;
const content = html`
<div class="wrapper" @click=${(ev) => ev.stopPropagation()}>
${(c.stateObj.state === "unavailable")
? html`
<span class="state">
unavailable
</span>
`
: html`
${((this._config.hide_when_off && c.isOff)
|| !c.hasSlider)
? ""
: slider }
${(this._config.hide_state || this._config.toggle)
? ""
: html`
<span class="state">
${c.string}
</span>
`}
${this._config.toggle
&& c.hasToggle
? toggle
: ""}
`}
</div>
`;
if (this._config.full_row)
return content;
return html`
<hui-generic-entity-row
.hass=${this.hass}
.config=${this._config}
> ${content} </hui-generic-entity-row>
`;
}
static get styles() {
return css`
.wrapper {
display: flex;
align-items: center;
height: 40px;
}
.state {
min-width: 45px;
text-align: end;
}
ha-entity-toggle {
margin-left: 8px;
}
ha-slider.full {
width: 100%;
}
`;
}
}
customElements.define('slider-entity-row', SliderEntityRow);

View File

@ -0,0 +1,34 @@
import {Controller} from "./controller.js";
export class MediaPlayerController extends Controller {
get _value() {
return (this.stateObj.is_volume_muted === "on")
? 0
: Math.ceil(this.stateObj.attributes.volume_level*100.0);
}
set _value(value) {
value = value/100.0;
this._hass.callService("media_player", "volume_set", {
entity_id: this.stateObj.entity_id,
volume_level: value,
});
}
get isOff() {
return this.stateObj.state === "off";
}
get string() {
if (this.stateObj.attributes.is_volume_muted)
return "-";
return !!this.stateObj.attributes.volume_level
? `${this.value} %`
: this._hass.localize("state.media_player.off");
}
get hasToggle() {
return false;
}
}