From 628767e9705f2a353ba2bcd1c6f1495680fd59f1 Mon Sep 17 00:00:00 2001 From: Igor Panteleyev Date: Mon, 29 Jul 2024 14:49:52 +0300 Subject: [PATCH 1/4] Add possibility to get WiFi SSID and password from entity --- README.md | 134 ++++++++++-------------------- package.json | 2 +- src/editor.ts | 29 ++++++- src/localize/languages/en.json | 24 ++++-- src/models/authentication-type.ts | 2 +- src/models/error.ts | 8 ++ src/models/generator.ts | 26 ++++-- src/qr-code-card.ts | 9 +- src/types/types.ts | 9 +- src/utils.ts | 41 ++++++++- src/validators.ts | 12 ++- 11 files changed, 172 insertions(+), 124 deletions(-) create mode 100644 src/models/error.ts diff --git a/README.md b/README.md index d86b158..d621875 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,6 @@ [![hacs_badge][hacs_shield]][hacs] [![GitHub Latest Release][releases_shield]][latest_release] -[hacs_shield]: https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=for-the-badge -[hacs]: https://github.com/hacs/integration - -[releases_shield]: https://img.shields.io/github/release/igor-panteleev/lovelace-qr-code-card.svg?style=for-the-badge -[latest_release]: https://github.com/igor-panteleev/lovelace-qr-code-card/releases/latest - # Lovelace QRCode Generator card This card provides a possibility to generate QRCode in Home Assistant interface. @@ -19,88 +13,46 @@ This card provides a possibility to generate QRCode in Home Assistant interface. ## Configuration - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeRequiredDefaultDescription
- General options -
titlestringnoemptyTitle for the card
sourcestringyestextCard source type. Options: text, entity, wifi
- Text mode options -
textstringyesQRCode example textText that will be used for QRCode generation
- Entity mode options -
entitystringyesemptyEntity that will be used for QRCode generation
- Wi-Fi mode options -
auth_typestringyesemptyWi-Fi network authentication type. Options: WEP, WPA, nopass
ssidstringyesemptyWi-Fi network ssid
passwordstringyes (except nopass authentication)emptyWi-Fi network password
is_hiddenbooleannoemptyIs Wi-Fi network is hidden
+### Main config + +| Key | Type | Required | Default | Description | +|-----------------------|------------------------------------------|-----------------|---------------------|-------------------------------------------------------------------------| +| *Generic options* | +| `title` | string | no | empty | Title for the card | +| `source` | string | yes | `text` | Card source type.
Options: `text,` `entity`, `wifi` | +| *Text mode options* | +| `text` | string | yes | QRCode example text | Text that will be used for QRCode generation | +| *Entity mode options* | +| `entity` | string | yes | empty | Entity that will be used for QRCode generation | +| *Wi-Fi mode options* | +| `auth_type` | string | yes | empty | Wi-Fi network authentication type.
Options: `WEP`, `WPA`, `nopass` | +| `ssid` | string \| [EntityConfig](#entity-config) | yes | empty | Wi-Fi network ssid | +| `password` | string \| [EntityConfig](#entity-config) | yes1 | empty | Wi-Fi network password | +| `is_hidden` | boolean | no | empty | Is Wi-Fi network is hidden | + +1Required for `WEP` and `WPA` authentication + +### Entity Config + +| Key | Type | Required | Description | +|-------------|--------|----------|--------------------------------------------------------------------------| +| `entity` | string | yes | Entity to get state from | +| `attribute` | string | no | Enables usage of a configured attribute instead of state of given entity | + + +### Example WiFi config +```yaml +type: custom:qr-code-card +source: wifi +title: My Awesom WiFi +auth_type: WPA +ssid: my_awesom_wifi +password: + entity: input_text.my_super_secure_password +``` + +[hacs_shield]: https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=for-the-badge +[hacs]: https://github.com/hacs/integration + +[releases_shield]: https://img.shields.io/github/release/igor-panteleev/lovelace-qr-code-card.svg?style=for-the-badge +[latest_release]: https://github.com/igor-panteleev/lovelace-qr-code-card/releases/latest diff --git a/package.json b/package.json index 42159f5..6881984 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qr-code-card", - "version": "v1.1.0", + "version": "v1.2.0", "description": "QR code card", "keywords": [ "home-assistant", diff --git a/src/editor.ts b/src/editor.ts index 317a7a8..d1b4b84 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -12,7 +12,7 @@ import { } from "./types/types"; import { localizeWithHass } from "./localize/localize"; import { SourceType } from "./models/source-type"; -import { AuthenticationType, is_password_protected } from "./models/authentication-type"; +import { AuthenticationType, isPasswordProtected } from "./models/authentication-type"; import { EDITOR_CUSTOM_ELEMENT_NAME } from "./const"; @@ -54,12 +54,18 @@ export class QRCodeCardEditor extends LitElement implements LovelaceCardEditor { get _ssid(): string { const config = this._config as WiFiSourceConfig | undefined; - return config?.ssid || ""; + if (typeof config?.ssid === "string") { + return config?.ssid || ""; + } + return "" } get _password(): string { const config = this._config as WiFiSourceConfig | undefined; - return config?.password || ""; + if (typeof config?.password === "string") { + return config?.password || ""; + } + return ""; } get _is_hidden(): boolean { @@ -72,6 +78,10 @@ export class QRCodeCardEditor extends LitElement implements LovelaceCardEditor { return config?.entity || "" } + private _isDisabled(): boolean { + return typeof this._config?.ssid !== "string" || typeof this._config?.password !== "string"; + } + private _localize(ts: TranslatableString): string { return localizeWithHass(ts, this.hass, this._config); } @@ -81,6 +91,13 @@ export class QRCodeCardEditor extends LitElement implements LovelaceCardEditor { return html``; } + if (this._isDisabled()) { + return html` +
+
${this._localize("editor.yaml_mode")}
+
`; + } + const entities = Object.keys(this.hass.states); return html` @@ -139,7 +156,7 @@ export class QRCodeCardEditor extends LitElement implements LovelaceCardEditor { .configValue=${"ssid"} @input=${this._valueChanged}> - ${is_password_protected(this._auth_type) ? html` + ${isPasswordProtected(this._auth_type) ? html`
{ @@ -33,8 +34,13 @@ abstract class QRCodeGenerator { try { return QRCode.toDataURL(this.input, this.quality); } - catch (e: any) { - throw new Error(localize("generation.error")) + catch (e: unknown) { + if (e instanceof TranslatableError) { + throw e + } else if (e instanceof Error) { + throw new TranslatableError(["generation.error", "{message}", e.message]) + } + throw new TranslatableError("generation.unknown_error") } } @@ -64,10 +70,12 @@ class WiFiQRCodeGenerator extends QRCodeGenerator { } protected get input(): string { - let text = `WIFI:T:${this.config.auth_type || ""};S:${this._escape(this.config.ssid || "")};`; + const ssid = getValueFromConfig(this.hass, this.config, "ssid"); + let text = `WIFI:T:${this.config.auth_type || ""};S:${this._escape(ssid)};`; - if (is_password_protected(this.config.auth_type)) { - text += `P:${this._escape(this.config.password || "")};` + if (isPasswordProtected(this.config.auth_type)) { + const password = getValueFromConfig(this.hass, this.config, "password"); + text += `P:${this._escape(password)};` } if (!this.config.is_hidden) { @@ -80,7 +88,7 @@ class WiFiQRCodeGenerator extends QRCodeGenerator { class EntityQRCodeGenerator extends QRCodeGenerator { protected get input(): string { - return this.hass?.states[this.config.entity].state + return getValueFromConfig(this.hass, this.config, "entity") } } @@ -94,7 +102,7 @@ const generatorMap = new Map { const generatorCls = generatorMap.get(config.source); - if (!generatorCls) throw new Error(localize("validation.source.invalid")); + if (!generatorCls) throw new TranslatableError("validation.source.invalid"); return await (new generatorCls(hass, config)).generate(); } diff --git a/src/qr-code-card.ts b/src/qr-code-card.ts index 33ce68c..c2ef88b 100644 --- a/src/qr-code-card.ts +++ b/src/qr-code-card.ts @@ -13,6 +13,7 @@ import { generateQR } from "./models/generator"; import { validateConfig } from "./validators"; import { SourceType } from "./models/source-type"; import { CARD_CUSTOM_ELEMENT_NAME, EDITOR_CUSTOM_ELEMENT_NAME } from "./const"; +import { TranslatableError } from "./models/error"; console.info( `%c QR-CODE-GENERATOR %c ${version} `, @@ -77,10 +78,10 @@ export class QRCodeCard extends LitElement { try { this.dataUrl = await generateQR(this.hass, this.config); } catch (e: unknown) { - if (e instanceof Error) { + if (e instanceof TranslatableError) { this.errors = [e.message] } else { - this.errors = ['An unknown error occurred']; + this.errors = [this._localize("generation.unknown_error")]; } } } @@ -147,8 +148,8 @@ export class QRCodeCard extends LitElement { flex-direction: column; flex: 1; position: relative; - padding: 0px; - border-radius: 4px; + padding: 0; + border-radius: 6px; overflow: hidden; } .qrcode { diff --git a/src/types/types.ts b/src/types/types.ts index 274fce7..7df1cc6 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -19,6 +19,11 @@ export type DataUrl = string; export type QRCodeGeneratorClass = new (...args: any[]) => T; export type QRCodeValidatorClass = new (...args: any[]) => T; +export interface EntityConfig { + readonly entity: string; + readonly attribute?: string; +} + export interface BaseQRCodeCardConfig extends LovelaceCardConfig { readonly language?: Language; readonly title?: string; @@ -31,8 +36,8 @@ export interface TextSourceConfig extends BaseQRCodeCardConfig { export interface WiFiSourceConfig extends BaseQRCodeCardConfig { readonly auth_type: AuthenticationType; - readonly ssid: string; - readonly password?: string; + readonly ssid: string | EntityConfig; + readonly password?: string | EntityConfig; readonly is_hidden?: boolean; } diff --git a/src/utils.ts b/src/utils.ts index e5c173f..386da87 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,6 +2,7 @@ import { PropertyValues } from "@lit/reactive-element"; import { HomeAssistant } from "custom-card-helpers"; import { QRCodeCardConfig } from "./types/types" import { SourceType } from "./models/source-type"; +import { TranslatableError } from "./models/error"; export function hasConfigOrAnyEntityChanged( watchedEntities: string[], @@ -24,11 +25,43 @@ export function getWatchedEntities(config: QRCodeCardConfig): string[] { watchedEntities.add(config.entity); break; - // TODO: add support to use entities for WIFI QR code - // case SourceType.WIFI: - // if (config.ssid.hasOwnProperty("entity")) watchedEntities.add(config.ssid["entity"]); - // if (config.password.hasOwnProperty("entity")) watchedEntities.add(config.password["entity"]); + case SourceType.WIFI: + if (config.ssid.hasOwnProperty("entity")) watchedEntities.add(config.ssid["entity"]); + if (config.password.hasOwnProperty("entity")) watchedEntities.add(config.password["entity"]); } return [...watchedEntities]; } + +export function getValueFromConfig(hass: HomeAssistant, config: QRCodeCardConfig, property: string): string { + let result: string; + + const configProperty = config[property]; + if (configProperty === undefined) { + throw new TranslatableError(`validation.${property}.missing`) + } else if (typeof configProperty === "string") { + result = configProperty; + } else if (configProperty.hasOwnProperty("entity")) { + const entity = hass?.states[configProperty.entity] + if (entity === undefined) { + throw new TranslatableError([`validation.${property}.unknown_entity`, "{entity}", configProperty.entity]) + } + if (configProperty.attribute !== undefined) { + const attribute_value = entity.attributes[configProperty.attribute]; + if (attribute_value === undefined) { + throw new TranslatableError([`validation.${property}.unknown_attribute`, "{attribute}", configProperty.attribute]) + } + result = attribute_value.toString(); + } else { + const state = entity.state; + if (state === "unavailable") { + throw new TranslatableError([`validation.${property}.unavailable`, "{entity}", configProperty.entity]) + } + result = state; + } + } else { + throw new TranslatableError([`validation.${property}.unknown_type`, "{type}", typeof configProperty]) + } + + return result; +} diff --git a/src/validators.ts b/src/validators.ts index 480d1ac..f6dced6 100644 --- a/src/validators.ts +++ b/src/validators.ts @@ -8,7 +8,7 @@ import { } from "./types/types"; import { localize } from "./localize/localize"; import { SourceType } from "./models/source-type"; -import { AuthenticationType, is_password_protected } from "./models/authentication-type"; +import { AuthenticationType, isPasswordProtected } from "./models/authentication-type"; abstract class Validator { @@ -71,11 +71,17 @@ class WiFiValidator extends Validator { // Validate ssid if (!this.config.ssid) { errors.push("validation.ssid.missing"); + } else if (typeof this.config.ssid !== "string" && !this.config.ssid.hasOwnProperty("entity")) { + errors.push("validation.ssid.entity.missing") } // Validate password - if (is_password_protected(this.config.auth_type) && !this.config.password) { - errors.push("validation.password.missing"); + if (isPasswordProtected(this.config.auth_type)) { + if (!this.config.password) { + errors.push("validation.password.missing"); + } else if (typeof this.config.password !== "string" && !this.config.password.hasOwnProperty("entity")) { + errors.push("validation.password.entity.missing") + } } return errors; From 857c62ce375f593f67529db4971f24b8a91a10a4 Mon Sep 17 00:00:00 2001 From: Igor Panteleyev Date: Mon, 29 Jul 2024 17:57:09 +0300 Subject: [PATCH 2/4] Separate data builder and QRcode generator --- src/generator.ts | 34 ++++++++++++ src/models/data-builder.ts | 0 src/models/generator.ts | 108 ------------------------------------- 3 files changed, 34 insertions(+), 108 deletions(-) create mode 100644 src/generator.ts create mode 100644 src/models/data-builder.ts delete mode 100644 src/models/generator.ts diff --git a/src/generator.ts b/src/generator.ts new file mode 100644 index 0000000..638837a --- /dev/null +++ b/src/generator.ts @@ -0,0 +1,34 @@ +import QRCode from "qrcode"; + +import { DataUrl } from "../types/types"; +import { TranslatableError } from "./error"; + + +class QRCodeGenerator { + + protected readonly inputString: string; + + // TODO: make it configurable + protected readonly quality = { + margin: 1, + width: 500 + } + + public constructor(inputString: string) { + this.inputString = inputString; + } + + public async generate(): Promise { + try { + return QRCode.toDataURL(this.inputString, this.quality); + } + catch (e: unknown) { + throw new TranslatableError("generation.unknown_error") + } + } + +} + +export async function generateQR(inputString: string): Promise { + return await (new QRCodeGenerator(inputString)).generate(); +} diff --git a/src/models/data-builder.ts b/src/models/data-builder.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/models/generator.ts b/src/models/generator.ts deleted file mode 100644 index dd15adc..0000000 --- a/src/models/generator.ts +++ /dev/null @@ -1,108 +0,0 @@ -import QRCode from "qrcode"; - -import { - DataUrl, EntitySourceConfig, - QRCodeCardConfig, - QRCodeGeneratorClass, - TextSourceConfig, - WiFiSourceConfig, -} from "../types/types"; -import { isPasswordProtected } from "./authentication-type"; -import { SourceType } from "./source-type"; -import { getValueFromConfig } from "../utils" -import { HomeAssistant } from "custom-card-helpers"; -import { TranslatableError } from "./error"; - - -abstract class QRCodeGenerator { - - protected readonly config: T; - protected readonly hass: HomeAssistant; - - // TODO: make it configurable - protected readonly quality = { - margin: 1, - width: 500 - } - - public constructor(hass: HomeAssistant, config: T) { - this.hass = hass; - this.config = config; - } - - public async generate(): Promise { - try { - return QRCode.toDataURL(this.input, this.quality); - } - catch (e: unknown) { - if (e instanceof TranslatableError) { - throw e - } else if (e instanceof Error) { - throw new TranslatableError(["generation.error", "{message}", e.message]) - } - throw new TranslatableError("generation.unknown_error") - } - } - - protected abstract get input(): string - -} - - -class TextQRCodeGenerator extends QRCodeGenerator { - - protected get input(): string { - return this.config.text || ""; - } -} - - -class WiFiQRCodeGenerator extends QRCodeGenerator { - protected readonly special_chars = ['\\', ';', ',', '"', ':'] - - protected _escape(plain: string): string { - return this.special_chars.reduce( - (previousValue, currentValue) => { - return previousValue.replace(currentValue, '\\'+currentValue) - }, - plain - ); - } - - protected get input(): string { - const ssid = getValueFromConfig(this.hass, this.config, "ssid"); - let text = `WIFI:T:${this.config.auth_type || ""};S:${this._escape(ssid)};`; - - if (isPasswordProtected(this.config.auth_type)) { - const password = getValueFromConfig(this.hass, this.config, "password"); - text += `P:${this._escape(password)};` - } - - if (!this.config.is_hidden) { - text += "H:true" - } - - return text; - } -} - -class EntityQRCodeGenerator extends QRCodeGenerator { - protected get input(): string { - return getValueFromConfig(this.hass, this.config, "entity") - } -} - -const generatorMap = new Map>>([ - [SourceType.TEXT, TextQRCodeGenerator], - [SourceType.WIFI, WiFiQRCodeGenerator], - [SourceType.ENTITY, EntityQRCodeGenerator] -]); - - -export async function generateQR(hass: HomeAssistant, config: QRCodeCardConfig): Promise { - const generatorCls = generatorMap.get(config.source); - - if (!generatorCls) throw new TranslatableError("validation.source.invalid"); - - return await (new generatorCls(hass, config)).generate(); -} From 53901f50c49187ded47edeb3dde0a375d3fd5f1a Mon Sep 17 00:00:00 2001 From: Igor Panteleyev Date: Mon, 29 Jul 2024 17:57:56 +0300 Subject: [PATCH 3/4] Fix issue when card has an error and it don't rendered after page request --- src/qr-code-card.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/qr-code-card.ts b/src/qr-code-card.ts index c2ef88b..952e00a 100644 --- a/src/qr-code-card.ts +++ b/src/qr-code-card.ts @@ -9,7 +9,8 @@ import type {QRCodeCardConfig, TranslatableString } from "./types/types"; import { localize, localizeWithHass } from "./localize/localize"; import { getWatchedEntities, hasConfigOrAnyEntityChanged } from "./utils" import { version } from '../package.json'; -import { generateQR } from "./models/generator"; +import { getInputString } from "./models/data-builder"; +import { generateQR } from "./generator"; import { validateConfig } from "./validators"; import { SourceType } from "./models/source-type"; import { CARD_CUSTOM_ELEMENT_NAME, EDITOR_CUSTOM_ELEMENT_NAME } from "./const"; @@ -47,6 +48,7 @@ export class QRCodeCard extends LitElement { private config!: QRCodeCardConfig; private watchedEntities: string[] = []; + private inputString!: string; @property({ attribute: false }) public _hass!: HomeAssistant; @state() private errors: string[] = []; @state() private dataUrl = ""; @@ -76,7 +78,8 @@ export class QRCodeCard extends LitElement { private async _updateQR(): Promise { try { - this.dataUrl = await generateQR(this.hass, this.config); + this.inputString = getInputString(this.hass, this.config) + this.dataUrl = await generateQR(this.inputString); } catch (e: unknown) { if (e instanceof TranslatableError) { this.errors = [e.message] @@ -98,13 +101,15 @@ export class QRCodeCard extends LitElement { } protected async update(changedProperties: PropertyValues): Promise { - await this._updateQR(); + if (this.errors.length == 0) { + await this._updateQR(); + } super.update(changedProperties); } protected render(): TemplateResult { if (this.errors.length > 0) { - return this._showErrors(this.errors); + return this._showErrors(); } if (!this.dataUrl) { @@ -125,15 +130,16 @@ export class QRCodeCard extends LitElement { ` } - private _showErrors(errors: string[]): TemplateResult { - errors.forEach(e => console.error(e)); + private _showErrors(): TemplateResult { + this.errors.forEach(e => console.error(e)); const errorCard = document.createElement("hui-error-card") as LovelaceCard; - errorCard.setConfig({ - type: "error", - error: errors[0], - origConfig: this.config, + customElements.whenDefined("hui-error-card").then(() => { + errorCard.setConfig({ + type: "error", + error: this.errors[0], + origConfig: this.config, + }); }); - return html` ${errorCard} `; } From e9936ed5a6f77ff45b301afccef08c696a98debd Mon Sep 17 00:00:00 2001 From: Igor Panteleyev Date: Mon, 29 Jul 2024 17:58:39 +0300 Subject: [PATCH 4/4] Add debug mode --- src/editor.ts | 16 +++- src/generator.ts | 38 +++------- src/localize/languages/en.json | 8 +- src/models/data-builder.ts | 131 +++++++++++++++++++++++++++++++++ src/qr-code-card.ts | 1 + src/types/types.ts | 1 + src/utils.ts | 32 -------- src/validators.ts | 13 ++++ 8 files changed, 178 insertions(+), 62 deletions(-) diff --git a/src/editor.ts b/src/editor.ts index d1b4b84..2579b55 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -72,6 +72,11 @@ export class QRCodeCardEditor extends LitElement implements LovelaceCardEditor { const config = this._config as WiFiSourceConfig | undefined; return config?.is_hidden || false; } + + get _is_debug(): boolean { + const config = this._config as QRCodeCardConfig | undefined; + return config?.debug || false; + } get _entity(): string { const config = this._config as EntitySourceConfig | undefined; @@ -79,7 +84,7 @@ export class QRCodeCardEditor extends LitElement implements LovelaceCardEditor { } private _isDisabled(): boolean { - return typeof this._config?.ssid !== "string" || typeof this._config?.password !== "string"; + return this._config?.source === SourceType.WIFI && (typeof this._config?.ssid !== "string" || typeof this._config?.password !== "string"); } private _localize(ts: TranslatableString): string { @@ -200,6 +205,15 @@ export class QRCodeCardEditor extends LitElement implements LovelaceCardEditor { })}
` : ""} + +
+ + + +
`; } diff --git a/src/generator.ts b/src/generator.ts index 638837a..f292e1d 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -1,34 +1,18 @@ import QRCode from "qrcode"; -import { DataUrl } from "../types/types"; -import { TranslatableError } from "./error"; - - -class QRCodeGenerator { - - protected readonly inputString: string; - - // TODO: make it configurable - protected readonly quality = { - margin: 1, - width: 500 - } - - public constructor(inputString: string) { - this.inputString = inputString; - } - - public async generate(): Promise { - try { - return QRCode.toDataURL(this.inputString, this.quality); - } - catch (e: unknown) { - throw new TranslatableError("generation.unknown_error") - } - } +import { DataUrl } from "./types/types"; +import { TranslatableError } from "./models/error"; +const quality = { + margin: 1, + width: 500 } export async function generateQR(inputString: string): Promise { - return await (new QRCodeGenerator(inputString)).generate(); + try { + return QRCode.toDataURL(inputString, quality); + } + catch (e: unknown) { + throw new TranslatableError("generation.unknown_error") + } } diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 521219d..fada52d 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -13,7 +13,8 @@ "auth_type": "Authentication type (required)", "ssid": "SSID (required)", "password": "Password (required)", - "is_hidden": "Hidden SSID" + "is_hidden": "Hidden SSID", + "is_debug": "Debug mode (Show value for QR code)" }, "title": { "show_password": "Show password", @@ -31,9 +32,12 @@ "nopass": "None" } }, - "yaml_mode": "Getting SSID and password from entity is not supported in visual editor mode. Please switch to code editor mode." + "yaml_mode": "Configuring SSID and password from entity is not supported in visual editor mode. Please switch to code editor mode." }, "validation": { + "debug": { + "invalid": "Debug mode must be true or false" + }, "source": { "missing": "Missing property: source", "invalid": "Invalid source type" diff --git a/src/models/data-builder.ts b/src/models/data-builder.ts index e69de29..3b9f30e 100644 --- a/src/models/data-builder.ts +++ b/src/models/data-builder.ts @@ -0,0 +1,131 @@ +import { + EntitySourceConfig, + QRCodeCardConfig, + QRCodeGeneratorClass, + TextSourceConfig, + WiFiSourceConfig, +} from "../types/types"; +import { isPasswordProtected } from "./authentication-type"; +import { SourceType } from "./source-type"; +import { HomeAssistant } from "custom-card-helpers"; +import { TranslatableError } from "./error"; + + +abstract class QRCodeDataBuilder { + + protected readonly config: T; + protected readonly hass: HomeAssistant; + + public constructor(hass: HomeAssistant, config: T) { + this.hass = hass; + this.config = config; + } + + public getInputString(): string { + try { + return this._getInputString(); + } + catch (e: unknown) { + if (e instanceof TranslatableError) { + throw e + } else if (e instanceof Error) { + throw new TranslatableError(["generation.error", "{message}", e.message]) + } + throw new TranslatableError("generation.unknown_error") + } + } + + protected abstract _getInputString(): string + + protected _getValueFromConfig(property: string): string { + let result: string; + + const configProperty = this.config[property]; + if (configProperty === undefined) { + throw new TranslatableError(`validation.${property}.missing`) + } else if (typeof configProperty === "string") { + result = configProperty; + } else if (configProperty.hasOwnProperty("entity")) { + const entity = this.hass?.states[configProperty.entity] + if (entity === undefined) { + throw new TranslatableError([`validation.${property}.unknown_entity`, "{entity}", configProperty.entity]) + } + if (configProperty.attribute !== undefined) { + const attribute_value = entity.attributes[configProperty.attribute]; + if (attribute_value === undefined) { + throw new TranslatableError([`validation.${property}.unknown_attribute`, "{attribute}", configProperty.attribute]) + } + result = attribute_value.toString(); + } else { + const state = entity.state; + if (state === "unavailable") { + throw new TranslatableError([`validation.${property}.unavailable`, "{entity}", configProperty.entity]) + } + result = state; + } + } else { + throw new TranslatableError([`validation.${property}.unknown_type`, "{type}", typeof configProperty]) + } + + return result; + } +} + + +class TextQRCodeDataBuilder extends QRCodeDataBuilder { + + protected _getInputString(): string { + return this.config.text || ""; + } +} + + +class WiFiQRCodeDataBuilder extends QRCodeDataBuilder { + protected readonly special_chars = ['\\', ';', ',', '"', ':'] + + protected _escape(plain: string): string { + return this.special_chars.reduce( + (previousValue, currentValue) => { + return previousValue.replace(currentValue, '\\'+currentValue) + }, + plain + ); + } + + protected _getInputString(): string { + const ssid = this._getValueFromConfig("ssid"); + let text = `WIFI:T:${this.config.auth_type || ""};S:${this._escape(ssid)};`; + + if (isPasswordProtected(this.config.auth_type)) { + const password = this._getValueFromConfig("password"); + text += `P:${this._escape(password)};` + } + + if (this.config.is_hidden) { + text += "H:true" + } + + return text; + } +} + +class EntityQRCodeDataBuilder extends QRCodeDataBuilder { + protected _getInputString(): string { + return this._getValueFromConfig("entity") + } +} + +const configBuilderMapping = new Map>>([ + [SourceType.TEXT, TextQRCodeDataBuilder], + [SourceType.WIFI, WiFiQRCodeDataBuilder], + [SourceType.ENTITY, EntityQRCodeDataBuilder] +]); + + +export function getInputString(hass: HomeAssistant, config: QRCodeCardConfig): string { + const dataBuilderCls = configBuilderMapping.get(config.source); + + if (!dataBuilderCls) throw new TranslatableError("validation.source.invalid"); + + return new dataBuilderCls(hass, config).getInputString(); +} diff --git a/src/qr-code-card.ts b/src/qr-code-card.ts index 952e00a..d31af85 100644 --- a/src/qr-code-card.ts +++ b/src/qr-code-card.ts @@ -123,6 +123,7 @@ export class QRCodeCard extends LitElement { return html` ${(this.config?.title ?? "").length > 0 ? html`

${this.config.title}

`: ""} + ${(this.config?.debug ?? false) ? html`

Input string: ${this.inputString}

`: ""}
diff --git a/src/types/types.ts b/src/types/types.ts index 7df1cc6..6bf762a 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -26,6 +26,7 @@ export interface EntityConfig { export interface BaseQRCodeCardConfig extends LovelaceCardConfig { readonly language?: Language; + readonly debug?: boolean; readonly title?: string; readonly source: SourceType; } diff --git a/src/utils.ts b/src/utils.ts index 386da87..a598446 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -33,35 +33,3 @@ export function getWatchedEntities(config: QRCodeCardConfig): string[] { return [...watchedEntities]; } -export function getValueFromConfig(hass: HomeAssistant, config: QRCodeCardConfig, property: string): string { - let result: string; - - const configProperty = config[property]; - if (configProperty === undefined) { - throw new TranslatableError(`validation.${property}.missing`) - } else if (typeof configProperty === "string") { - result = configProperty; - } else if (configProperty.hasOwnProperty("entity")) { - const entity = hass?.states[configProperty.entity] - if (entity === undefined) { - throw new TranslatableError([`validation.${property}.unknown_entity`, "{entity}", configProperty.entity]) - } - if (configProperty.attribute !== undefined) { - const attribute_value = entity.attributes[configProperty.attribute]; - if (attribute_value === undefined) { - throw new TranslatableError([`validation.${property}.unknown_attribute`, "{attribute}", configProperty.attribute]) - } - result = attribute_value.toString(); - } else { - const state = entity.state; - if (state === "unavailable") { - throw new TranslatableError([`validation.${property}.unavailable`, "{entity}", configProperty.entity]) - } - result = state; - } - } else { - throw new TranslatableError([`validation.${property}.unknown_type`, "{type}", typeof configProperty]) - } - - return result; -} diff --git a/src/validators.ts b/src/validators.ts index f6dced6..42c7a72 100644 --- a/src/validators.ts +++ b/src/validators.ts @@ -27,6 +27,18 @@ abstract class Validator { } +class DebugModeValidator extends Validator { + protected _validate(): string[] { + const errors: string[] = []; + + if (this.config.debug !== undefined && typeof this.config.debug !== "boolean") { + errors.push("validation.debug.invalid"); + } + + return errors; + } +} + class SourceValidator extends Validator { protected _validate(): string[] { const errors: string[] = []; @@ -108,6 +120,7 @@ export function validateConfig(config: QRCodeCardConfig): string[] { const errors: TranslatableString[] = []; new SourceValidator(config).validate().forEach(e => errors.push(e)); + new DebugModeValidator(config).validate().forEach(e => errors.push(e)); if (errors.length == 0) { const validatorCls = validatorMap.get(config.source);