Total overhaul!
This commit is contained in:
parent
46be70ee49
commit
1d29e2365f
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
slider-entity-row.js binary
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
package.json
|
||||
webpack.config.js
|
42
Makefile
Normal file
42
Makefile
Normal 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,' $@
|
151
README.md
151
README.md
@ -1,105 +1,80 @@
|
||||
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
|
||||
```
|
||||
|
||||

|
||||
|
||||
Currenly supported entity domains:
|
||||
|
||||
- `light` - set brightness
|
||||
- `media_player` - set volume
|
||||
- `climate` - set temperature
|
||||
- `cover` - set position
|
||||
- `fan` - set speed (assumes first setting is `off`)
|
||||
- `input_number`
|
||||
- `input_select`
|
||||
- `input_number` - set value (only if `mode: slider`)
|
||||
- `input_select` - select option
|
||||
|
||||

|
||||

|
||||
|
||||
### 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
|
||||
- title: slider-entity-row
|
||||
cards:
|
||||
- type: entities
|
||||
title: Domains
|
||||
show_header_toggle: false
|
||||
entities:
|
||||
- input_number.slider
|
||||
|
||||
- type: section
|
||||
label: light
|
||||
- type: custom:slider-entity-row
|
||||
entity: light.bed_light
|
||||
- type: custom:slider-entity-row
|
||||
entity: light.ceiling_lights
|
||||
- type: custom:slider-entity-row
|
||||
entity: light.kitchen_lights
|
||||
|
||||
- type: section
|
||||
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
|
||||
|
||||
- type: section
|
||||
label: "full_row: true"
|
||||
- entity: light.bed_light
|
||||
- type: custom:slider-entity-row
|
||||
entity: light.bed_light
|
||||
full_row: true
|
||||
- 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
|
||||
type: entities
|
||||
title: Options
|
||||
entities:
|
||||
- type: custom:slider-entity-row
|
||||
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
|
||||
entity: light.ceiling_lights
|
||||
name: hide_when_off
|
||||
hide_when_off: true
|
||||
- type: custom:slider-entity-row
|
||||
entity: light.ceiling_lights
|
||||
name: hide_when_off + toggle
|
||||
hide_when_off: true
|
||||
toggle: true
|
||||
- type: section
|
||||
label: full_row
|
||||
- type: custom:slider-entity-row
|
||||
entity: light.bed_light
|
||||
name: hide_state
|
||||
full_row: true
|
||||
```
|
||||
|
||||
### Extra options
|
||||
`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
35
src/climate-controller.js
Normal 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
59
src/controller.js
Normal 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
40
src/cover-controller.js
Normal 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
43
src/fan-controller.js
Normal 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;
|
||||
}
|
||||
|
||||
}
|
44
src/input-number-controller.js
Normal file
44
src/input-number-controller.js
Normal 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;
|
||||
}
|
||||
|
||||
}
|
41
src/input-select-controller.js
Normal file
41
src/input-select-controller.js
Normal 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
37
src/light-controller.js
Normal 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
119
src/main.js
Normal 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);
|
34
src/media-player-controller.js
Normal file
34
src/media-player-controller.js
Normal 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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user