From e7166bd3e7d4138adc950d4c1b5f784cc9f21f00 Mon Sep 17 00:00:00 2001 From: nearxos Date: Tue, 6 Jan 2026 19:30:17 +0200 Subject: [PATCH] Initial commit: Light Button Card for Home Assistant --- .gitignore | 8 + README.md | 117 ++++++++++++ example-configuration.yaml | 20 +++ hacs.json | 6 + package.json | 31 ++++ src/light-button-card.ts | 356 +++++++++++++++++++++++++++++++++++++ tsconfig.json | 18 ++ webpack.config.js | 32 ++++ 8 files changed, 588 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 example-configuration.yaml create mode 100644 hacs.json create mode 100644 package.json create mode 100644 src/light-button-card.ts create mode 100644 tsconfig.json create mode 100644 webpack.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4c92bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +*.log +.DS_Store +*.swp +*.swo +*~ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..423ba58 --- /dev/null +++ b/README.md @@ -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 + diff --git a/example-configuration.yaml b/example-configuration.yaml new file mode 100644 index 0000000..29e2d69 --- /dev/null +++ b/example-configuration.yaml @@ -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 + diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..d30d242 --- /dev/null +++ b/hacs.json @@ -0,0 +1,6 @@ +{ + "name": "Light Button Card", + "render_readme": true, + "filename": "light-button-card.js" +} + diff --git a/package.json b/package.json new file mode 100644 index 0000000..78f26e5 --- /dev/null +++ b/package.json @@ -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" + } +} + diff --git a/src/light-button-card.ts b/src/light-button-card.ts new file mode 100644 index 0000000..87bbfae --- /dev/null +++ b/src/light-button-card.ts @@ -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; +} + +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 { + 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 { + 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` + +
+
+ +
+
+ ${this._config.show_name && this._config.name + ? html`
${this._config.name}
` + : ''} +
${state}
+
+
+ `; + } +} + +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` +
+ + + + + + + + + + + + + + Show Name + +
+ `; + } + + 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', +}); + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5d2fe81 --- /dev/null +++ b/tsconfig.json @@ -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"] +} + diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..269a06e --- /dev/null +++ b/webpack.config.js @@ -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, + }, +}; +