diff --git a/.gitignore b/.gitignore
index a00fdccc..f916afff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
/node_modules/
# Don't add directory /dist/ to .gitignore, as it is used by the build script
+dist/mushroom-strategy.js
diff --git a/package-lock.json b/package-lock.json
index 0497aa99..c40623ab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,10 +6,11 @@
"packages": {
"": {
"name": "mushroom-strategy",
- "version": "2.3.4",
+ "version": "2.3.5",
"license": "MIT",
"dependencies": {
- "deepmerge": "^4"
+ "deepmerge": "^4",
+ "lit": "^3.0.0"
},
"devDependencies": {
"@types/semver": "^7.7.0",
@@ -408,6 +409,21 @@
"node": ">=10"
}
},
+ "node_modules/@lit-labs/ssr-dom-shim": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz",
+ "integrity": "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@lit/reactive-element": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.1.tgz",
+ "integrity": "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.4.0"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -569,6 +585,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT"
+ },
"node_modules/@types/unist": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
@@ -2755,6 +2777,37 @@
"uc.micro": "^2.0.0"
}
},
+ "node_modules/lit": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.1.tgz",
+ "integrity": "sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit/reactive-element": "^2.1.0",
+ "lit-element": "^4.2.0",
+ "lit-html": "^3.3.0"
+ }
+ },
+ "node_modules/lit-element": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.1.tgz",
+ "integrity": "sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.4.0",
+ "@lit/reactive-element": "^2.1.0",
+ "lit-html": "^3.3.0"
+ }
+ },
+ "node_modules/lit-html": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.1.tgz",
+ "integrity": "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@types/trusted-types": "^2.0.2"
+ }
+ },
"node_modules/loader-runner": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
diff --git a/package.json b/package.json
index bf541e04..f86af3c6 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,8 @@
"url": "https://github.com/DigiLive/mushroom-strategy"
},
"dependencies": {
- "deepmerge": "^4"
+ "deepmerge": "^4",
+ "lit": "^3.0.0"
},
"devDependencies": {
"@types/semver": "^7.7.0",
diff --git a/src/editor/hui-mushroom-strategy-editor.ts b/src/editor/hui-mushroom-strategy-editor.ts
new file mode 100644
index 00000000..0776af8d
--- /dev/null
+++ b/src/editor/hui-mushroom-strategy-editor.ts
@@ -0,0 +1,428 @@
+import { LitElement, html, css, TemplateResult } from 'lit';
+import { customElement, property, state } from 'lit/decorators.js';
+import { fireEvent } from '../utilities/fire-event';
+import { StrategyConfig } from '../types/strategy/strategy-generics';
+
+export interface LovelaceStrategyEditor extends HTMLElement {
+ hass?: any;
+ config?: any;
+ setConfig(config: any): void;
+ configChanged?: (config: any) => void;
+}
+
+@customElement("hui-mushroom-strategy-editor")
+export class HuiMushroomStrategyEditor
+ extends LitElement
+ implements LovelaceStrategyEditor
+{
+ @property({ attribute: false }) public hass!: any;
+ @state() private _config?: any;
+ @state() private _selectedConfigArea?: string;
+
+ static get styles() {
+ return css`
+ .card-config {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ .section {
+ border: 1px solid var(--divider-color);
+ border-radius: 8px;
+ padding: 16px;
+ margin-bottom: 16px;
+ }
+
+ .section-header {
+ font-weight: bold;
+ margin-bottom: 12px;
+ color: var(--primary-text-color);
+ }
+
+ .form-row {
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+ }
+
+ .form-row label {
+ width: 200px;
+ color: var(--primary-text-color);
+ }
+
+ ha-switch {
+ margin-left: auto;
+ }
+
+ ha-textfield {
+ flex: 1;
+ margin-left: 16px;
+ }
+
+ ha-select {
+ flex: 1;
+ margin-left: 16px;
+ }
+ `;
+ }
+
+ public setConfig(config: any): void {
+ console.log('Editor setConfig called with:', config);
+ this._config = config || { type: 'custom:mushroom-strategy', options: {} };
+ }
+
+ public configChanged(config: any): void {
+ console.log('Editor configChanged called with:', config);
+ this._config = config || { type: 'custom:mushroom-strategy', options: {} };
+ this.requestUpdate();
+ }
+
+ private _valueChanged(ev: CustomEvent): void {
+ if (!this._config) return;
+
+ const target = ev.target as any;
+ const configPath = target.configPath;
+ const value = target.value;
+
+ if (!configPath) return;
+
+ const newConfig = JSON.parse(JSON.stringify(this._config));
+
+ // Ensure the options structure exists (flat structure for strategies)
+ if (!newConfig.options) {
+ newConfig.options = {};
+ }
+
+ // Navigate to the correct path and set the value
+ let current = newConfig.options;
+ const pathParts = configPath.split('.');
+
+ for (let i = 0; i < pathParts.length - 1; i++) {
+ if (!current[pathParts[i]]) {
+ current[pathParts[i]] = {};
+ }
+ current = current[pathParts[i]];
+ }
+
+ current[pathParts[pathParts.length - 1]] = value;
+
+ fireEvent(this, "config-changed", { config: newConfig });
+ }
+
+ protected render(): TemplateResult {
+ if (!this._config) {
+ // Initialize with default config if none provided
+ this._config = {
+ type: 'custom:mushroom-strategy',
+ options: {}
+ };
+ }
+
+ // For strategies, options are directly under the config
+ const options = this._config.options || {};
+
+ console.log('Editor rendering with config:', this._config);
+ console.log('Options:', options);
+
+ return html`
+
+
+ Mushroom Strategy Configuration
+
+ ${this._renderGeneralSection(options)}
+ ${this._renderHomeViewSection(options)}
+ ${this._renderChipsSection(options)}
+ ${this._renderAreasSection(options)}
+ ${this._renderDomainsSection(options)}
+ ${this._renderViewsSection(options)}
+
+ `;
+ }
+
+ private _renderGeneralSection(options: StrategyConfig): TemplateResult {
+ return html`
+
+ `;
+ }
+
+ private _renderHomeViewSection(options: StrategyConfig): TemplateResult {
+ const homeView = options.home_view || { hidden: [], stack_count: { _: 2 } };
+
+ return html`
+
+
+
+
+
+
+
+
+ ${this._renderHiddenSections(homeView.hidden || [])}
+
+ `;
+ }
+
+ private _renderHiddenSections(hidden: string[]): TemplateResult {
+ const sections = ['areas', 'areasTitle', 'chips', 'greeting', 'persons'];
+
+ return html`
+
+
+ ${sections.map(section => html`
+
+
+
+
+ `)}
+
+ `;
+ }
+
+ private _toggleHiddenSection(ev: CustomEvent): void {
+ const target = ev.target as any;
+ const section = target.section;
+ const isChecked = target.checked;
+
+ if (!this._config) return;
+
+ const newConfig = JSON.parse(JSON.stringify(this._config));
+
+ // Ensure the options structure exists (flat structure for strategies)
+ if (!newConfig.options) {
+ newConfig.options = {};
+ }
+
+ const hidden = newConfig.options.home_view?.hidden || [];
+
+ if (isChecked && !hidden.includes(section)) {
+ hidden.push(section);
+ } else if (!isChecked) {
+ const index = hidden.indexOf(section);
+ if (index > -1) {
+ hidden.splice(index, 1);
+ }
+ }
+
+ if (!newConfig.options.home_view) {
+ newConfig.options.home_view = {};
+ }
+ newConfig.options.home_view.hidden = hidden;
+
+ fireEvent(this, "config-changed", { config: newConfig });
+ }
+
+ private _renderChipsSection(options: StrategyConfig): TemplateResult {
+ const chips = options.chips || {};
+
+ return html`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ private _renderAreasSection(options: StrategyConfig): TemplateResult {
+ return html`
+
+
+
+
+
+
+ Mushroom Area Card
+ Home Assistant Area Card
+
+
+
+ `;
+ }
+
+ private _renderDomainsSection(options: StrategyConfig): TemplateResult {
+ const domains = options.domains || {};
+ const supportedDomains = ['light', 'switch', 'fan', 'cover', 'climate', 'lock', 'camera', 'vacuum', 'scene'] as const;
+
+ return html`
+
+
+
+ ${supportedDomains.map(domain => html`
+
+
+
+
+ `)}
+
+ `;
+ }
+
+ private _toggleDomainHidden(ev: CustomEvent): void {
+ const target = ev.target as any;
+ const configPath = target.configPath;
+ const isChecked = target.checked;
+
+ if (!this._config) return;
+
+ const newConfig = JSON.parse(JSON.stringify(this._config));
+
+ // Ensure the options structure exists (flat structure for strategies)
+ if (!newConfig.options) {
+ newConfig.options = {};
+ }
+
+ // Navigate to the correct path and set the hidden value (opposite of checked)
+ let current = newConfig.options;
+ const pathParts = configPath.split('.');
+
+ for (let i = 0; i < pathParts.length - 1; i++) {
+ if (!current[pathParts[i]]) {
+ current[pathParts[i]] = {};
+ }
+ current = current[pathParts[i]];
+ }
+
+ current[pathParts[pathParts.length - 1]] = !isChecked;
+
+ fireEvent(this, "config-changed", { config: newConfig });
+ }
+
+ private _renderViewsSection(options: StrategyConfig): TemplateResult {
+ const views = options.views || {};
+ const supportedViews = ['home', 'light', 'switch', 'fan', 'cover', 'climate', 'lock', 'camera', 'vacuum', 'scene'] as const;
+
+ return html`
+
+
+
+ ${supportedViews.map(view => html`
+
+
+
+
+ `)}
+
+ `;
+ }
+
+ private _toggleViewHidden(ev: CustomEvent): void {
+ const target = ev.target as any;
+ const configPath = target.configPath;
+ const isChecked = target.checked;
+
+ if (!this._config) return;
+
+ const newConfig = JSON.parse(JSON.stringify(this._config));
+
+ // Ensure the options structure exists (flat structure for strategies)
+ if (!newConfig.options) {
+ newConfig.options = {};
+ }
+
+ // Navigate to the correct path and set the hidden value (opposite of checked)
+ let current = newConfig.options;
+ const pathParts = configPath.split('.');
+
+ for (let i = 0; i < pathParts.length - 1; i++) {
+ if (!current[pathParts[i]]) {
+ current[pathParts[i]] = {};
+ }
+ current = current[pathParts[i]];
+ }
+
+ current[pathParts[pathParts.length - 1]] = !isChecked;
+
+ fireEvent(this, "config-changed", { config: newConfig });
+ }
+}
+
+// Ensure custom element is registered
+if (!customElements.get("hui-mushroom-strategy-editor")) {
+ customElements.define("hui-mushroom-strategy-editor", HuiMushroomStrategyEditor);
+}
\ No newline at end of file
diff --git a/src/mushroom-strategy.ts b/src/mushroom-strategy.ts
index 5d425859..3244e3fc 100644
--- a/src/mushroom-strategy.ts
+++ b/src/mushroom-strategy.ts
@@ -1,6 +1,7 @@
import { HassServiceTarget } from 'home-assistant-js-websocket';
import HeaderCard from './cards/HeaderCard';
import SensorCard from './cards/SensorCard';
+import './editor/hui-mushroom-strategy-editor';
import { Registry } from './Registry';
import { LovelaceCardConfig } from './types/homeassistant/data/lovelace/config/card';
import { LovelaceConfig } from './types/homeassistant/data/lovelace/config/types';
@@ -268,8 +269,20 @@ class MushroomStrategy extends HTMLTemplateElement {
logMessage(lvlError, 'Error while handling persistent notifications for Mushroom Strategy', e);
}
}
+
+ static getConfigElement() {
+ return document.createElement("hui-mushroom-strategy-editor");
+ }
+
+ static getStubConfig() {
+ return {
+ type: 'custom:mushroom-strategy',
+ options: {}
+ };
+ }
}
+// Register the strategy
customElements.define('ll-strategy-mushroom-strategy', MushroomStrategy);
const STRATEGY_VERSION = 'v2.3.5';
@@ -278,3 +291,15 @@ console.info(
'color: white; background: coral; font-weight: 700;',
'color: coral; background: white; font-weight: 700;'
);
+
+// Debug: Test editor registration
+setTimeout(() => {
+ const strategy = customElements.get('ll-strategy-mushroom-strategy');
+ const editor = customElements.get('hui-mushroom-strategy-editor');
+ console.log('Mushroom Strategy Debug:', {
+ strategyRegistered: !!strategy,
+ editorRegistered: !!editor,
+ hasGetConfigElement: !!(strategy as any)?.getConfigElement,
+ hasGetStubConfig: !!(strategy as any)?.getStubConfig
+ });
+}, 1000);
diff --git a/src/utilities/fire-event.ts b/src/utilities/fire-event.ts
new file mode 100644
index 00000000..3e8bc491
--- /dev/null
+++ b/src/utilities/fire-event.ts
@@ -0,0 +1,21 @@
+export const fireEvent = (
+ node: HTMLElement,
+ type: string,
+ detail: any,
+ options?: {
+ bubbles?: boolean;
+ cancelable?: boolean;
+ composed?: boolean;
+ }
+): Event => {
+ options = options || {};
+ detail = detail === null || detail === undefined ? {} : detail;
+ const event = new CustomEvent(type, {
+ bubbles: options.bubbles === undefined ? true : options.bubbles,
+ cancelable: Boolean(options.cancelable),
+ composed: options.composed === undefined ? true : options.composed,
+ detail,
+ });
+ node.dispatchEvent(event);
+ return event;
+};
diff --git a/tsconfig.json b/tsconfig.json
index be86809c..5be25efb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -15,13 +15,16 @@
// 4. Interop and Imports
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
- // 5. Type Checking and Strictness
+ // 5. Experimental Features
+ "experimentalDecorators": true,
+ "useDefineForClassFields": false,
+ // 6. Type Checking and Strictness
"strict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"strictNullChecks": true,
- // 6. Project Structure and Build Integrity
+ // 7. Project Structure and Build Integrity
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},