Initial commit: Light Button Card for Home Assistant
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
117
README.md
Normal file
117
README.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Light Button Card
|
||||
|
||||
A custom Lovelace card for Home Assistant that combines a button entity (for toggling) with a binary sensor entity (for status display). Perfect for controlling lights or other devices via PLC relays.
|
||||
|
||||
## Features
|
||||
|
||||
- **Button Toggle**: Click the card to send a toggle command to your button entity
|
||||
- **Status Display**: Shows the current state from your binary sensor entity
|
||||
- **Custom Icons**: Support for custom icons with separate icons for on/off states
|
||||
- **Visual Feedback**: Status indicator dot and icon color changes based on state
|
||||
- **Card Editor**: Full UI editor support in Lovelace
|
||||
|
||||
## Installation
|
||||
|
||||
### Manual Installation
|
||||
|
||||
1. Copy `dist/light-button-card.js` to your `www/community/light-button-card/` directory in Home Assistant
|
||||
2. Add the resource to your Lovelace configuration:
|
||||
|
||||
```yaml
|
||||
resources:
|
||||
- url: /local/light-button-card/light-button-card.js
|
||||
type: module
|
||||
```
|
||||
|
||||
### HACS Installation (if published)
|
||||
|
||||
1. Go to HACS → Frontend
|
||||
2. Click the three dots menu → Custom repositories
|
||||
3. Add this repository
|
||||
4. Install "Light Button Card"
|
||||
5. Refresh your browser
|
||||
|
||||
## Configuration
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
```yaml
|
||||
type: custom:light-button-card
|
||||
button_entity: button.plc_light_1
|
||||
binary_sensor_entity: binary_sensor.plc_light_1_status
|
||||
```
|
||||
|
||||
### Full Configuration Options
|
||||
|
||||
```yaml
|
||||
type: custom:light-button-card
|
||||
button_entity: button.plc_light_1 # Required: Button entity to toggle
|
||||
binary_sensor_entity: binary_sensor.plc_light_1_status # Required: Binary sensor for status
|
||||
name: Living Room Light # Optional: Display name
|
||||
icon: mdi:lightbulb # Optional: Default icon (MDI format)
|
||||
icon_on: mdi:lightbulb # Optional: Icon when state is "on"
|
||||
icon_off: mdi:lightbulb-outline # Optional: Icon when state is "off"
|
||||
show_name: true # Optional: Show/hide name (default: true)
|
||||
```
|
||||
|
||||
## Icon Configuration
|
||||
|
||||
Icons use Material Design Icons (MDI) format. You can use any icon from [Material Design Icons](https://materialdesignicons.com/).
|
||||
|
||||
Examples:
|
||||
- `mdi:lightbulb` - Light bulb
|
||||
- `mdi:lightbulb-outline` - Light bulb outline
|
||||
- `mdi:lamp` - Lamp
|
||||
- `mdi:ceiling-light` - Ceiling light
|
||||
- `mdi:wall-sconce` - Wall sconce
|
||||
|
||||
## Usage Example
|
||||
|
||||
```yaml
|
||||
type: custom:light-button-card
|
||||
button_entity: button.plc_kitchen_light
|
||||
binary_sensor_entity: binary_sensor.plc_kitchen_light_status
|
||||
name: Kitchen Light
|
||||
icon_on: mdi:lightbulb
|
||||
icon_off: mdi:lightbulb-outline
|
||||
show_name: true
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Button Entity**: When you click the card, it calls `button.press` service on the specified button entity, which sends a command to your PLC to toggle the relay.
|
||||
|
||||
2. **Binary Sensor Entity**: The card continuously monitors the binary sensor entity to display the current state (on/off) of the PLC output.
|
||||
|
||||
3. **Visual Feedback**:
|
||||
- The icon changes color and style based on the state
|
||||
- A status indicator dot shows green when on, gray when off
|
||||
- The state text displays the current state
|
||||
|
||||
## Development
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
The compiled file will be in `dist/light-button-card.js`.
|
||||
|
||||
### Development Mode
|
||||
|
||||
```bash
|
||||
npm run watch
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Home Assistant 2023.1.0 or later
|
||||
- Button entity (for toggling)
|
||||
- Binary sensor entity (for status)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
20
example-configuration.yaml
Normal file
20
example-configuration.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
# Example Lovelace configuration for Light Button Card
|
||||
# Add this to your Lovelace dashboard
|
||||
|
||||
type: custom:light-button-card
|
||||
button_entity: button.plc_light_1
|
||||
binary_sensor_entity: binary_sensor.plc_light_1_status
|
||||
name: Living Room Light
|
||||
icon_on: mdi:lightbulb
|
||||
icon_off: mdi:lightbulb-outline
|
||||
show_name: true
|
||||
|
||||
# Another example with different icon
|
||||
# type: custom:light-button-card
|
||||
# button_entity: button.plc_light_2
|
||||
# binary_sensor_entity: binary_sensor.plc_light_2_status
|
||||
# name: Kitchen Light
|
||||
# icon_on: mdi:ceiling-light
|
||||
# icon_off: mdi:ceiling-light-outline
|
||||
# show_name: true
|
||||
|
||||
6
hacs.json
Normal file
6
hacs.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Light Button Card",
|
||||
"render_readme": true,
|
||||
"filename": "light-button-card.js"
|
||||
}
|
||||
|
||||
31
package.json
Normal file
31
package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "homeassistant-light-buttons",
|
||||
"version": "1.0.0",
|
||||
"description": "Custom Lovelace card for Home Assistant with button toggle and binary sensor status",
|
||||
"main": "dist/light-button-card.js",
|
||||
"scripts": {
|
||||
"build": "webpack --mode production",
|
||||
"watch": "webpack --mode development --watch"
|
||||
},
|
||||
"keywords": [
|
||||
"home-assistant",
|
||||
"lovelace",
|
||||
"custom-card"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"ts-loader": "^9.5.0",
|
||||
"typescript": "^5.0.0",
|
||||
"webpack": "^5.88.0",
|
||||
"webpack-cli": "^5.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lit": "^2.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"home-assistant-js-websocket": "^8.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
356
src/light-button-card.ts
Normal file
356
src/light-button-card.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
import { LitElement, html, css, TemplateResult } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
// Type definitions for Home Assistant
|
||||
interface HomeAssistant {
|
||||
states: { [key: string]: HassEntity };
|
||||
callService(domain: string, service: string, data?: any): Promise<void>;
|
||||
}
|
||||
|
||||
interface HassEntity {
|
||||
entity_id: string;
|
||||
state: string;
|
||||
attributes: { [key: string]: any };
|
||||
}
|
||||
|
||||
interface LovelaceCardEditor {
|
||||
setConfig(config: any): void;
|
||||
}
|
||||
|
||||
interface LightButtonCardConfig {
|
||||
type: string;
|
||||
button_entity: string;
|
||||
binary_sensor_entity: string;
|
||||
icon?: string;
|
||||
icon_on?: string;
|
||||
icon_off?: string;
|
||||
name?: string;
|
||||
show_name?: boolean;
|
||||
}
|
||||
|
||||
// Helper function to fire events
|
||||
function fireEvent(node: HTMLElement, type: string, detail?: any): void {
|
||||
const event = new CustomEvent(type, {
|
||||
detail,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
});
|
||||
node.dispatchEvent(event);
|
||||
}
|
||||
|
||||
@customElement('light-button-card')
|
||||
export class LightButtonCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state() private _config!: LightButtonCardConfig;
|
||||
|
||||
static getStubConfig(): Partial<LightButtonCardConfig> {
|
||||
return {
|
||||
button_entity: 'button.example',
|
||||
binary_sensor_entity: 'binary_sensor.example',
|
||||
icon: 'mdi:lightbulb',
|
||||
name: 'Light',
|
||||
show_name: true,
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: LightButtonCardConfig): void {
|
||||
if (!config.button_entity || !config.binary_sensor_entity) {
|
||||
throw new Error('button_entity and binary_sensor_entity are required');
|
||||
}
|
||||
this._config = {
|
||||
icon: 'mdi:lightbulb',
|
||||
icon_on: 'mdi:lightbulb',
|
||||
icon_off: 'mdi:lightbulb-outline',
|
||||
name: '',
|
||||
show_name: true,
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-card {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
ha-card:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
ha-card:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
ha-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
color: var(--primary-color);
|
||||
transition: color 0.3s, transform 0.2s;
|
||||
}
|
||||
|
||||
.icon-container.on ha-icon {
|
||||
color: var(--paper-item-icon-active-color, #fdd835);
|
||||
filter: drop-shadow(0 0 8px rgba(253, 216, 53, 0.6));
|
||||
}
|
||||
|
||||
.icon-container.off ha-icon {
|
||||
color: var(--disabled-text-color, #9e9e9e);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: var(--disabled-text-color, #9e9e9e);
|
||||
border: 2px solid var(--card-background-color, #ffffff);
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.status-indicator.on {
|
||||
background-color: #4caf50;
|
||||
box-shadow: 0 0 8px rgba(76, 175, 80, 0.6);
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--primary-text-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _getState(): string {
|
||||
if (!this.hass || !this._config.binary_sensor_entity) {
|
||||
return 'unknown';
|
||||
}
|
||||
const stateObj = this.hass.states[this._config.binary_sensor_entity];
|
||||
if (!stateObj) {
|
||||
return 'unknown';
|
||||
}
|
||||
return stateObj.state;
|
||||
}
|
||||
|
||||
private _isOn(): boolean {
|
||||
const state = this._getState();
|
||||
return state === 'on' || state === 'true' || state === '1';
|
||||
}
|
||||
|
||||
private _getIcon(): string {
|
||||
if (this._isOn()) {
|
||||
return this._config.icon_on || this._config.icon || 'mdi:lightbulb';
|
||||
}
|
||||
return this._config.icon_off || this._config.icon || 'mdi:lightbulb-outline';
|
||||
}
|
||||
|
||||
private async _handleClick(): Promise<void> {
|
||||
if (!this.hass || !this._config.button_entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonEntity = this.hass.states[this._config.button_entity];
|
||||
if (!buttonEntity) {
|
||||
console.error(`Button entity ${this._config.button_entity} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the button press service
|
||||
await this.hass.callService('button', 'press', {
|
||||
entity_id: this._config.button_entity,
|
||||
});
|
||||
|
||||
// Fire event for card updates
|
||||
fireEvent(this, 'hass-more-info', { entityId: this._config.button_entity });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._config || !this.hass) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const isOn = this._isOn();
|
||||
const state = this._getState();
|
||||
const icon = this._getIcon();
|
||||
|
||||
return html`
|
||||
<ha-card @click=${this._handleClick}>
|
||||
<div class="button-container">
|
||||
<div class="icon-container ${isOn ? 'on' : 'off'}">
|
||||
<ha-icon .icon=${icon}></ha-icon>
|
||||
<div class="status-indicator ${isOn ? 'on' : ''}"></div>
|
||||
</div>
|
||||
${this._config.show_name && this._config.name
|
||||
? html`<div class="name">${this._config.name}</div>`
|
||||
: ''}
|
||||
<div class="state-text">${state}</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'light-button-card': LightButtonCard;
|
||||
}
|
||||
}
|
||||
|
||||
// Card editor for Lovelace UI
|
||||
@customElement('light-button-card-editor')
|
||||
export class LightButtonCardEditor extends LitElement implements LovelaceCardEditor {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@property({ attribute: false }) private _config?: LightButtonCardConfig;
|
||||
|
||||
public setConfig(config: LightButtonCardConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
.card-config {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="card-config">
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._config.button_entity}
|
||||
.label=${'Button Entity'}
|
||||
.configValue=${'button_entity'}
|
||||
.required=${true}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-entity-picker>
|
||||
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._config.binary_sensor_entity}
|
||||
.label=${'Binary Sensor Entity'}
|
||||
.configValue=${'binary_sensor_entity'}
|
||||
.required=${true}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-entity-picker>
|
||||
|
||||
<ha-textfield
|
||||
.label=${'Name (optional)'}
|
||||
.value=${this._config.name || ''}
|
||||
.configValue=${'name'}
|
||||
@input=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
|
||||
<ha-textfield
|
||||
.label=${'Icon (MDI icon name, optional)'}
|
||||
.value=${this._config.icon || ''}
|
||||
.configValue=${'icon'}
|
||||
@input=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
|
||||
<ha-textfield
|
||||
.label=${'Icon On (MDI icon name, optional)'}
|
||||
.value=${this._config.icon_on || ''}
|
||||
.configValue=${'icon_on'}
|
||||
@input=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
|
||||
<ha-textfield
|
||||
.label=${'Icon Off (MDI icon name, optional)'}
|
||||
.value=${this._config.icon_off || ''}
|
||||
.configValue=${'icon_off'}
|
||||
@input=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
|
||||
<ha-switch
|
||||
.checked=${this._config.show_name !== false}
|
||||
.configValue=${'show_name'}
|
||||
@change=${this._valueChanged}
|
||||
>
|
||||
<span>Show Name</span>
|
||||
</ha-switch>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: Event): void {
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = ev.target as any;
|
||||
const configValue = target.configValue;
|
||||
|
||||
if (configValue === 'show_name') {
|
||||
this._config = {
|
||||
...this._config,
|
||||
[configValue]: target.checked,
|
||||
};
|
||||
} else {
|
||||
this._config = {
|
||||
...this._config,
|
||||
[configValue]: target.value || target.checked,
|
||||
};
|
||||
}
|
||||
|
||||
fireEvent(this, 'config-changed', { config: this._config });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'light-button-card-editor': LightButtonCardEditor;
|
||||
}
|
||||
}
|
||||
|
||||
// Register the card editor
|
||||
if (customElements.get('light-button-card-editor') === undefined) {
|
||||
customElements.define('light-button-card-editor', LightButtonCardEditor);
|
||||
}
|
||||
|
||||
// Card configuration helper for Lovelace
|
||||
(window as any).customCards = (window as any).customCards || [];
|
||||
(window as any).customCards.push({
|
||||
type: 'light-button-card',
|
||||
name: 'Light Button Card',
|
||||
description: 'A card with button toggle and binary sensor status',
|
||||
});
|
||||
|
||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
32
webpack.config.js
Normal file
32
webpack.config.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: './src/light-button-card.ts',
|
||||
output: {
|
||||
filename: 'light-button-card.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
library: {
|
||||
type: 'module',
|
||||
},
|
||||
},
|
||||
experiments: {
|
||||
outputModule: true,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
},
|
||||
mode: 'production',
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user