{"id":13588767,"url":"https://github.com/homebridge/plugin-ui-utils","last_synced_at":"2025-04-05T14:05:55.408Z","repository":{"id":47555413,"uuid":"309038272","full_name":"homebridge/plugin-ui-utils","owner":"homebridge","description":"Create fully customisable configuration user interfaces for Homebridge plugins.","archived":false,"fork":false,"pushed_at":"2025-02-22T17:21:56.000Z","size":274,"stargazers_count":35,"open_issues_count":0,"forks_count":8,"subscribers_count":9,"default_branch":"latest","last_synced_at":"2025-03-29T13:06:59.841Z","etag":null,"topics":["homebridge","homebridge-plugin","homebridge-ui"],"latest_commit_sha":null,"homepage":"https://homebridge.io","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/homebridge.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-11-01T06:37:13.000Z","updated_at":"2025-02-22T17:21:59.000Z","dependencies_parsed_at":"2023-12-16T20:21:51.960Z","dependency_job_id":"10cb3c10-4779-4060-98ca-bd57bed1ebde","html_url":"https://github.com/homebridge/plugin-ui-utils","commit_stats":{"total_commits":62,"total_committers":9,"mean_commits":6.888888888888889,"dds":0.3709677419354839,"last_synced_commit":"bfc8fdc2396676e7c89c87ae9cec89e7bc4bf072"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/homebridge%2Fplugin-ui-utils","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/homebridge%2Fplugin-ui-utils/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/homebridge%2Fplugin-ui-utils/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/homebridge%2Fplugin-ui-utils/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/homebridge","download_url":"https://codeload.github.com/homebridge/plugin-ui-utils/tar.gz/refs/heads/latest","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247345852,"owners_count":20924102,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["homebridge","homebridge-plugin","homebridge-ui"],"created_at":"2024-08-01T15:06:54.736Z","updated_at":"2025-04-05T14:05:55.401Z","avatar_url":"https://github.com/homebridge.png","language":"TypeScript","readme":"\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://homebridge.io\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/homebridge/branding/latest/logos/homebridge-color-round-stylized.png\" height=\"140\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\u003cspan align=\"center\"\u003e\n\n# Plugin UI Utils\n\n[![npm](https://badgen.net/npm/v/@homebridge/plugin-ui-utils)](https://www.npmjs.com/package/@homebridge/plugin-ui-utils)\n[![npm](https://badgen.net/npm/dt/@homebridge/plugin-ui-utils)](https://www.npmjs.com/package/@homebridge/plugin-ui-utils)\n[![Discord](https://img.shields.io/discord/432663330281226270?color=728ED5\u0026logo=discord\u0026label=discord)](https://discord.gg/kqNCe2D)\n\n\u003c/span\u003e\n\nThe package assists plugin developers creating fully customisable configuration user interfaces for their plugins.\n\n- [Implementation](#implementation)\n  - [Project Layout](#project-layout)\n- [User Interface API](#user-interface-api)\n  - [Config](#config)\n  - [Requests](#requests)\n  - [Toast Notifications](#toast-notifications)\n  - [Modal](#modal)\n  - [Forms](#forms)\n  - [Events](#events)\n  - [Plugin / Server Information](#plugin--server-information)\n- [Server API](#server-api)\n  - [Setup](#setup)\n  - [Request Handling](#request-handling)\n  - [Request Error Handling](#request-error-handling)\n  - [Push Events](#push-events)\n  - [Server Information](#server-information)\n- [Examples](#examples)\n- [Development](#development)\n\n## Implementation\n\nA plugin's custom user interface has two main components:\n\n- [User Interface](#user-interface-api) - this is the HTML / CSS / JavaScript code the users interact with\n- [Server](#server-api) - this is an optional server side script that provides endpoints the UI can call\n\n### Project Layout\n\nA custom UI should be published under a directory named `homebridge-ui`:\n\n- `homebridge-ui/public/index.html` - required - this is the plugin UI entry point.\n- `homebridge-ui/public/` - you can store any other assets (`.css`, `.js`, images etc.) in the public folder.\n- `homebridge-ui/server.js` - optional - this is the server side script containing API endpoints for your plugin UI.\n- `config.schema.json` - required - set `customUi` to `true` in the schema to enable custom UI.\n\nBasic structure example:\n\n```bash\nhomebridge-example-plugin/\n├── homebridge-ui\n│   ├── public\n│   │   └── index.html\n│   └── server.js\n├── config.schema.json\n├── package.json\n```\n\nYou may customise the location of the `homebridge-ui` by setting the `customUiPath` property in the `config.schema.json`. For example: `\"customUiPath\": \"./dist/homebridge-ui\"`.\n\n## User Interface API\n\nA plugin's custom user interface is displayed inside an iframe in the settings modal, in place of the schema-generated form.\n\nThe user interface API is provided to the plugin's custom UI via the `window.homebridge` object. This is injected into the plugin's custom UI during render.\n\n\u003cp align=\"center\"\u003e\n\u003cimg width=\"700px\" src=\"https://user-images.githubusercontent.com/3979615/97826339-73d83500-1d15-11eb-8a14-a2a8e4895959.png\"\u003e\n\u003c/p\u003e\n\nNote:\n\n- Developers are free to use front end frameworks such as Angular, Vue, or React to create the plugin's custom user interface.\n- Developers should make use [Bootstrap 5](https://getbootstrap.com/docs) CSS classes, as these will automatically be styled and themed correctly. There is no need to include the boostrap css yourself, this will be injected by the Homebridge UI during render.\n- As the user interface is displayed in an isolated iframe, you can safely use any custom JavaScript and CSS.\n- The `index.html` file should not include `\u003chtml\u003e`, `\u003chead\u003e`, or `\u003cbody\u003e` tags, as these are added by the Homebridge UI during the render process.\n- You may include external assets in your HTML.\n\nExample `index.html`:\n\n```html\n\u003clink rel=\"stylesheet\" href=\"your-plugin.css\"\u003e\n\n\u003cdiv class=\"card\"\u003e\n  \u003cdiv class=\"form-group\"\u003e\n    \u003clabel for=\"exampleInputEmail1\"\u003eEmail address\u003c/label\u003e\n    \u003cinput type=\"email\" class=\"form-control\" id=\"exampleInputEmail1\" aria-describedby=\"emailHelp\"\u003e\n    \u003csmall id=\"emailHelp\" class=\"form-text text-muted\"\u003eHelp text...\u003c/small\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\n\u003cscript\u003e\n(async () =\u003e {\n  // get the current homebridge config\n  const pluginConfig = await homebridge.getPluginConfig();\n\n  // make requests to your server.js script\n  const result = await homebridge.request('/hello', { name: 'world' });\n})();\n\u003c/script\u003e\n```\n\n### Config\n\n#### `homebridge.getPluginConfig`\n\n\u003e `homebridge.getPluginConfig(): Promise\u003cPluginConfig[]\u003e;`\n\nReturns a promise that resolves an array of accessory or platform config blocks for the plugin.\n\nAn empty array will be returned if the plugin is not currently configured.\n\n```ts\nconst pluginConfigBlocks = await homebridge.getPluginConfig()\n// [{ platform: 'ExamplePlatform', name: 'example' }]\n```\n\n#### `homebridge.updatePluginConfig`\n\n\u003e `homebridge.updatePluginConfig(pluginConfig: PluginConfig[]): Promise\u003cPluginConfig[]\u003e;`\n\nUpdate the plugin config.\n\n- `pluginConfig`: A full array of platform and accessory config blocks.\n\nThis should be called whenever a change to the config is made.\n\nThis does not save the plugin config to disk.\n\nExisting blocks not included will be removed.\n\n```ts\nconst pluginConfig = [\n  {\n    name: 'my light 1',\n    accessory: 'ExampleAccessory'\n  },\n  {\n    name: 'my light 2',\n    accessory: 'ExampleAccessory'\n  }\n]\n\nawait homebridge.updatePluginConfig(pluginConfig)\n```\n\n#### `homebridge.savePluginConfig`\n\n\u003e `homebridge.savePluginConfig(): Promise\u003cvoid\u003e`\n\nSaves the plugin config changes to the Homebridge `config.json`. This is the equivalent of clicking the _Save_ button.\n\nThis should be used sparingly, for example, after an access token is generated.\n\nYou must call `await homebridge.updatePluginConfig()` first.\n\n```ts\n// update config first!\nawait homebridge.updatePluginConfig(pluginConfig)\n\n// save config\nawait homebridge.savePluginConfig()\n```\n\n#### `homebridge.getPluginConfigSchema`\n\n\u003e `homebridge.getPluginConfigSchema(): Promise\u003cPluginSchema\u003e;`\n\nReturns the plugin's config.schema.json.\n\n```ts\nconst schema = await homebridge.getPluginConfigSchema()\n```\n\n#### `homebridge.getCachedAccessories`\n\n\u003e `homebridge.getCachedAccessories(): Promise\u003cCachedAccessory[]\u003e;`\n\nReturns the cached accessories for the plugin\n\n```ts\nconst cachedAccessories = await homebridge.getCachedAccessories()\n```\n\n### Requests\n\nThis allows the custom UI to make API requests to their `server.js` script.\n\n#### `homebridge.request`\n\n\u003e `homebridge.request(path: string, body?: any): Promise\u003cany\u003e`\n\nMake a request to the plugin's server side script.\n\n- `path`: the path handler on the server that the request should be sent to\n- `body`: an optional payload\n\nReturns a promise with the response from the server.\n\nUser Interface Example:\n\n```ts\nconst response = await homebridge.request('/hello', { who: 'world' })\nconsole.log(response) // the response from the server\n```\n\nThe corresponding code in the `server.js` file would look like this:\n\n```js\n// server side request handler\nthis.onRequest('/hello', async (payload) =\u003e {\n  console.log(payload) // the payload sent from the UI\n  return { hello: 'user' }\n})\n```\n\n### Toast Notifications\n\nToast notifications are the pop-up notifications displayed in the bottom right corner. A plugin's custom UI can generate custom notifications with custom content.\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://user-images.githubusercontent.com/3979615/97829910-7e97c780-1d1f-11eb-95ff-7d85d883b44c.png\"\u003e\n\u003c/p\u003e\n\n#### `homebridge.toast.success`\n\n\u003e `homebridge.toast.success(message: string, title?: string): void`\n\nShows a green \"success\" notification.\n\n- `message`: the toast content\n- `title`: an optional title\n\n#### `homebridge.toast.error`\n\n\u003e `homebridge.toast.error(message: string, title?: string): void`\n\nShows a red \"error\" notification.\n\n- `message`: the toast content\n- `title`: an optional title\n\n#### `homebridge.toast.warning`\n\n\u003e `homebridge.toast.warning(message: string, title?: string): void`\n\nShows an amber \"warning\" notification.\n\n- `message`: the toast content\n- `title`: an optional title\n\n#### `homebridge.toast.info`\n\n\u003e `homebridge.toast.info(message: string, title?: string): void`\n\nShows a blue \"info\" notification.\n\n- `message`: the toast content\n- `title`: an optional title\n\n### Modal\n\n#### `homebridge.closeSettings`\n\n\u003e `homebridge.closeSettings(): void`\n\nClose the settings modal.\n\nThis action does not save any config changes.\n\n```ts\nhomebridge.closeSettings()\n```\n\n#### `homebridge.showSpinner`\n\n\u003e `homebridge.showSpinner(): void`\n\nDisplays a spinner / loading overlay, preventing user input until cleared with `homebridge.hideSpinner`.\n\n```ts\n// show the spinner overlay\nhomebridge.showSpinner()\n\n// wait for the request to process\nawait homebridge.request('/hello')\n\n// hide the spinner overlay\nhomebridge.hideSpinner()\n```\n\n#### `homebridge.hideSpinner`\n\n\u003e `homebridge.hideSpinner(): void`\n\nHide the spinner / loading overlay.\n\n```ts\nhomebridge.hideSpinner()\n```\n\n#### `homebridge.disableSaveButton`\n\n\u003e `homebridge.disableSaveButton(): void`\n\nDisables the save button in the settings modal.\n\n```ts\nhomebridge.disableSaveButton()\n```\n\n#### `homebridge.enableSaveButton`\n\n\u003e `homebridge.enableSaveButton(): void`\n\nEnables the save button in the settings modal.\n\n```ts\nhomebridge.enableSaveButton()\n```\n\n### Forms\n\nThe custom user interface allows you to create two types of forms:\n\n1. A form based on your plugin's `config.schema.json` file\n   - User input is automatically mapped to the plugin config object\n   - You can listen for change events from your custom user interface\n   - The schema must contain all config options\n2. A standalone form\n   - Not linked to your `config.schema.json` form in any way\n   - You must listen for change events, process the event, and update the plugin config\n   - The form does not need to include all config options\n\nDevelopers are also able to create their own forms using HTML.\n\n#### `homebridge.showSchemaForm`\n\n\u003e `homebridge.showSchemaForm(): void`\n\nShow the schema-generated form below the custom user interface.\nThis feature only works for platform plugins that have set `singular` = `true` in their config.schema.json file.\n\n```ts\nhomebridge.showSchemaForm()\n```\n\nWhen enabling the schema form, you should listen for the `configChanged` event to keep your config in sync. This event is triggered whenever the user makes a change in the schema-generated form (250ms debounce).\n\n```ts\nwindow.homebridge.addEventListener('configChanged', (event: MessageEvent) =\u003e {\n  console.log('Updated config:', event.data)\n})\n```\n\n#### `homebridge.hideSchemaForm`\n\n\u003e `homebridge.hideSchemaForm(): void`\n\nHides the schema-generated form.\n\n```ts\nhomebridge.hideSchemaForm()\n```\n\n#### `homebridge.createForm`\n\n\u003e `homebridge.createForm(schema: FormSchema, data: any, submitButton?: string, cancelButton?: string): IHomebridgeUiFormHelper;`\n\nCreate a new standalone form. You may pass in an arbitrary schema using the same options as the [config.schema.json](https://developers.homebridge.io/#/config-schema).\n\nOnly one standalone form can be displayed at a time. The main config-schema based form cannot be shown while a standalone form is being displayed.\n\n- `schema`: The [form schema object](https://developers.homebridge.io/#/config-schema), may also contain layout metadata\n- `data`: The initial form data\n- `submitButton`: String. Optional label for a submit button, if not provided, no submit button will be displayed\n- `cancelButton`: String. Optional label for a cancel button, if not provided, no cancel button will be displayed\n\nExample:\n\n```ts\n// create the form\nconst myForm = homebridge.createForm(\n  {\n    schema: {\n      type: 'object',\n      properties: {\n        name: {\n          title: 'Name',\n          type: 'string',\n          required: true,\n        }\n      }\n    },\n    layout: null,\n    form: null,\n  },\n  {\n    name: 'initial name value'\n  }\n)\n\n// watch for change events\nmyForm.onChange((change) =\u003e {\n  console.log(change)\n})\n\n// watch for submit button click events\nmyForm.onSubmit((form) =\u003e {\n  console.log(form)\n})\n\n// watch for cancel button click events\nmyForm.onCancel((form) =\u003e {\n  console.log(form)\n})\n\n// stop listening to change events and hide the form\nmyForm.end()\n```\n\n### Events\n\nThe `homebridge` object is an [EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget), this allows you to use the browsers built in [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) and [removeEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener) functions to subscribe and unsubscribe from events.\n\n#### Ready Event\n\nCalled when the Homebridge UI has completed rendering the plugin's custom UI.\n\n```ts\nhomebridge.addEventListener('ready', () =\u003e {\n  // do something with event\n})\n```\n\n#### Custom Events\n\nCustom events can be pushed from the plugin's `server.js` script.\n\nUI Example:\n\n```ts\nhomebridge.addEventListener('my-event', (event) =\u003e {\n  console.log(event.data) // the event payload from the server\n})\n```\n\nThe corresponding code in the `server.js` file would look like this:\n\n```ts\nthis.pushEvent('my-event', { some: 'data' })\n```\n\n### Plugin / Server Information\n\n#### `homebridge.plugin`\n\n\u003e `homebridge.plugin`\n\nIs an object that contains plugin metadata.\n\n```ts\n{\n  name: string;\n  description: string;\n  installedVersion: string;\n  latestVersion: string;\n  verifiedPlugin: boolean;\n  updateAvailable: boolean;\n  publicPackage: boolean;\n  links: {\n    npm: string;\n    homepage?: string;\n  }\n}\n```\n\n#### homebridge.serverEnv\n\n\u003e `homebridge.serverEnv`\n\nIs an object containing some server metadata\n\n```ts\n{\n  env: {\n    platform: string // darwin, win32, linux, freebsd etc.\n    nodeVersion: string // Node.js version\n  }\n}\n```\n\n## Server API\n\nTo provide server API endpoints that can be called from the custom UI, a plugin must place a `server.js` file in the `homebridge-ui` directory.\n\nYou will need to include the `@homebridge/plugin-ui-utils` library as a prod dependency:\n\n```\nnpm install --save @homebridge/plugin-ui-utils\n```\n\nNote:\n\n- This `server.js` script will be spawned as a child process when the plugin's settings modal is opened, and is terminated when the settings modal is closed.\n- The `server.js` script must create a new instance of a class that extends `HomebridgePluginUiServer` from the `@homebridge/plugin-ui-utils` library.\n- This file will be spawned as a child process when the plugin's settings modal is opened, and is terminated when the settings modal is closed.\n- The server side script must extend the class provided by the `@homebridge/plugin-ui-utils` library.\n\nExample `server.js`:\n\n```js\nimport { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils'\n\n// your class MUST extend the HomebridgePluginUiServer\nclass UiServer extends HomebridgePluginUiServer {\n  constructor () {\n    // super must be called first\n    super()\n\n    // Example: create api endpoint request handlers (example only)\n    this.onRequest('/hello', this.handleHelloRequest.bind(this))\n\n    // this.ready() must be called to let the UI know you are ready to accept api calls\n    this.ready()\n  }\n\n  /**\n   * Example only.\n   * Handle requests made from the UI to the `/hello` endpoint.\n   */\n  async handleHelloRequest(payload) {\n    return { hello: 'world' }\n  }\n}\n\n// start the instance of the class\n(() =\u003e {\n  return new UiServer;\n})();\n```\n\n### Setup\n\n#### `this.ready`\n\n\u003e `this.ready(): void`\n\nLet the UI know the server is ready to accept requests.\n\n```ts\nthis.ready()\n```\n\n### Request Handling\n\n#### `this.onRequest`\n\n\u003e `this.onRequest(path: string, fn: RequestHandler)`\n\nHandle requests sent from the UI to the given path.\n\n- `path`: the request path name\n- `fn`: a function to handle the incoming requests\n\nThe value returned/resolved from the request handler function will be sent back to the UI as the request response.\n\nExample creating a request handler on the server:\n\n```ts\n// server side code\nthis.onRequest('/hello', async (payload) =\u003e {\n  console.log(payload) // the payload sent from the UI\n  return { hello: 'user' }\n})\n```\n\nThe corresponding call in the UI to send requests to this endpoint:\n\n```ts\n// ui code\nconst response = await homebridge.request('/hello', { who: 'world' })\nconsole.log(response) // the response from the server\n```\n\n### Request Error Handling\n\nIf you need to throw an error during your request, you should throw an instance of `RequestError` instead of a normal `Error`:\n\nExample:\n\n```ts\n// server side code\nimport { RequestError } from '@homebridge/plugin-ui-utils'\n\nthis.onRequest('/hello', async (payload) =\u003e {\n  // something went wrong, throw a RequestError:\n  throw new RequestError('Something went wrong!', { status: 404 })\n})\n```\n\nYou can then catch this in the UI:\n\n```ts\ntry {\n  await homebridge.request('/hello', { who: 'world' })\n} catch (e) {\n  console.log(e.message) // 'Something went wrong!'\n  console.log(e.error) // { status: 404 }\n}\n```\n\nUncaught errors in event handlers, or errors thrown using `new Error` will still result in the waiting promise in the UI being rejected, however the error stack trace will also be shown in the Homebridge logs which should be avoided.\n\n### Push Events\n\n#### `this.pushEvent`\n\n\u003e `this.pushEvent(event: string, data: any)`\n\nPush events allow you to send data to the UI, without needed the UI to request it first.\n\n- `event`: a string to describe the event type\n- `data`: any data to send as an event payload to the UI.\n\nExample pushing an event payload to the UI:\n\n```ts\nthis.pushEvent('my-event', { some: 'data' })\n```\n\nThe corresponding code to watch for the event in the UI:\n\n```ts\nhomebridge.addEventListener('my-event', (event) =\u003e {\n  console.log(event.data) // the event payload from the server\n})\n```\n\n### Server Information\n\n#### `this.homebridgeStoragePath`\n\n\u003e `this.homebridgeStoragePath: string`\n\nReturns the Homebridge instance's current storage path.\n\n```ts\nconst storagePath = this.homebridgeStoragePath\n```\n\n#### `this.homebridgeConfigPath`\n\n\u003e `this.homebridgeConfigPath: string`\n\nReturns the path to the Homebridge `config.json` file:\n\n```ts\nconst configPath = this.homebridgeConfigPath\n```\n\n#### `this.homebridgeUiVersion`\n\n\u003e `this.homebridgeUiVersion: string`\n\nReturns the version of the Homebridge UI:\n\n```ts\nconst uiVersion = this.homebridgeUiVersion\n```\n\n## Examples\n\n- [Basic Example](./examples/basic-ui-server) - demos a minimal custom user interface, interacting with server side scripts, updating the plugin config, and using toast notifications.\n- [Push Events](./examples/push-events) - demos how to send push events from the server, and listen for them in the custom user interface.\n\nA full list of plugins that have implemented the custom user interface can be found [here](https://www.npmjs.com/package/@homebridge/plugin-ui-utils?activeTab=dependents).\n\n##### homebridge-mercedesme\n\nThe [homebridge-mercedesme](https://github.com/SeydX/homebridge-mercedesme) plugin by [@SeydX](https://github.com/SeydX) allows users to pair their vehicle using a custom user interface:\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://raw.githubusercontent.com/SeydX/homebridge-mercedesme/beta/images/hb_mercedesme_ui.gif\" width=\"600px\"\u003e\n\u003c/p\u003e\n\n##### homebridge-bravia-tvos\n\nThe [homebridge-bravia-tvos](https://github.com/SeydX/homebridge-bravia-tvos) plugin by [@SeydX](https://github.com/SeydX) allows users to pair and dynamically configure a user's TV using a custom user interface:\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://user-images.githubusercontent.com/3979615/99958753-0a13ee00-2dde-11eb-95fb-69a896d37545.png\" width=\"600px\"\u003e\n\u003c/p\u003e\n\n##### homebridge-electra-smart\n\nThe [homebridge-electra-smart](https://github.com/nitaybz/homebridge-electra-smart) plugin by [nitaybz](https://github.com/nitaybz) allows users to request an OTP and enter it in exchange for an authentication token:\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://user-images.githubusercontent.com/3979615/99959242-be157900-2dde-11eb-8114-6394da2a2e14.png\" width=\"600px\"\u003e\n\u003c/p\u003e\n\n## Development\n\nFor hints and tips on how to develop your custom user interface, see [DEVELOPMENT.md](./DEVELOPMENT.md).\n","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhomebridge%2Fplugin-ui-utils","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhomebridge%2Fplugin-ui-utils","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhomebridge%2Fplugin-ui-utils/lists"}