diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md
new file mode 100644
index 0000000000..f088f4c019
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md
@@ -0,0 +1,85 @@
+---
+name: Report a bug with the UI, Frontend or Lovelace
+about: Report an issue related to the Home Assistant frontend.
+labels: bug
+---
+
+## Checklist
+
+- [ ] I have updated to the latest available Home Assistant version.
+- [ ] I have cleared the cache of my browser.
+- [ ] I have tried a different browser to see if it is related to my browser.
+
+## The problem
+
+
+
+## Expected behavior
+
+
+
+## Steps to reproduce
+
+
+
+## Environment
+
+
+- Home Assistant release with the issue:
+- Last working Home Assistant release (if known):
+- UI Type (States or Lovelace):
+- Browser and browser version:
+- Operating system:
+
+## Problem-relevant configuration
+
+
+```yaml
+
+```
+
+## Javascript errors shown in your browser console/inspector
+
+
+```txt
+
+```
+
+## Additional information
+
diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
new file mode 100644
index 0000000000..634724fcbe
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
@@ -0,0 +1,25 @@
+---
+name: Request a feature for the UI, Frontend or Lovelace
+about: Request an new feature for the Home Assistant frontend.
+labels: feature request
+---
+
+## The request
+
+
+
+## The alternatives
+
+
+
+## Additional information
+
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index a293b99f4a..0000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,78 +0,0 @@
----
-name: Bug report
-about: Create a report to help us improve
-title: ""
-labels: bug
-assignees: ""
----
-
-
-
-**Checklist:**
-
-- [ ] I updated to the latest version available
-- [ ] I cleared the cache of my browser
-
-**Home Assistant release with the issue:**
-
-
-
-**Last working Home Assistant release (if known):**
-
-**UI (States or Lovelace UI?):**
-
-
-
-**Browser and Operating System:**
-
-
-
-**Description of problem:**
-
-
-
-**Expected behaviour:**
-
-
-
-**Relevant config:**
-
-
-
-**Steps to reproduce this problem:**
-
-
-
-**Javascript errors shown in the web inspector (if applicable):**
-
-```
-
-```
-
-**Additional information:**
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..7581c03f17
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,14 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Report a bug that is NOT related to the UI, Frontend or Lovelace
+ url: https://github.com/home-assistant/home-assistant/issues
+ about: This is the issue tracker for our frontend. Please report other issues with the backend repository.
+ - name: Report incorrect or missing information on our website
+ url: https://github.com/home-assistant/home-assistant.io/issues
+ about: Our documentation has its own issue tracker. Please report issues with the website there.
+ - name: I have a question or need support
+ url: https://www.home-assistant.io/help
+ about: We use GitHub for tracking bugs, check our website for resources on getting help.
+ - name: I'm unsure where to go
+ url: https://www.home-assistant.io/join-chat
+ about: If you are unsure where to go, then joining our chat is recommended; Just ask!
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index 51d1465c16..0000000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for this project
-title: ""
-labels: feature request
-assignees: ""
----
-
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-**Additional context**
-Add any other context or screenshots about the feature request here.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000000..f73933940c
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,77 @@
+
+## Breaking change
+
+
+
+## Proposed change
+
+
+
+## Type of change
+
+
+- [ ] Dependency upgrade
+- [ ] Bugfix (non-breaking change which fixes an issue)
+- [ ] New feature (thank you!)
+- [ ] Breaking change (fix/feature causing existing functionality to break)
+- [ ] Code quality improvements to existing code or addition of tests
+
+## Example configuration
+
+
+```yaml
+
+```
+
+## Additional information
+
+
+- This PR fixes or closes issue: fixes #
+- This PR is related to issue:
+- Link to documentation pull request:
+
+## Checklist
+
+
+- [ ] The code change is tested and works locally.
+- [ ] There is no commented out code in this PR.
+- [ ] Tests have been added to verify that the new code works.
+
+If user exposed functionality or configuration variables are added/changed:
+
+- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]
+
+
+[docs-repository]: https://github.com/home-assistant/home-assistant.io
diff --git a/.github/lock.yml b/.github/lock.yml
new file mode 100644
index 0000000000..63db6533f2
--- /dev/null
+++ b/.github/lock.yml
@@ -0,0 +1,27 @@
+# Configuration for Lock Threads - https://github.com/dessant/lock-threads
+
+# Number of days of inactivity before a closed issue or pull request is locked
+daysUntilLock: 1
+
+# Skip issues and pull requests created before a given timestamp. Timestamp must
+# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
+skipCreatedBefore: 2020-01-01
+
+# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
+exemptLabels: []
+
+# Label to add before locking, such as `outdated`. Set to `false` to disable
+lockLabel: false
+
+# Comment to post before locking. Set to `false` to disable
+lockComment: false
+
+# Assign `resolved` as the reason for locking. Set to `false` to disable
+setLockReason: false
+
+# Limit to only `issues` or `pulls`
+only: pulls
+
+# Optionally, specify configuration settings just for `issues` or `pulls`
+issues:
+ daysUntilLock: 30
diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 0000000000..dc0896c22c
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,56 @@
+# Configuration for probot-stale - https://github.com/probot/stale
+
+# Number of days of inactivity before an Issue or Pull Request becomes stale
+daysUntilStale: 90
+
+# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
+# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
+daysUntilClose: 7
+
+# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
+onlyLabels: []
+
+# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
+exemptLabels:
+ - feature request
+ - Help wanted
+ - to do
+
+# Set to true to ignore issues in a project (defaults to false)
+exemptProjects: true
+
+# Set to true to ignore issues in a milestone (defaults to false)
+exemptMilestones: true
+
+# Set to true to ignore issues with an assignee (defaults to false)
+exemptAssignees: false
+
+# Label to use when marking as stale
+staleLabel: stale
+
+# Comment to post when marking as stale. Set to `false` to disable
+markComment: >
+ There hasn't been any activity on this issue recently. Due to the high number
+ of incoming GitHub notifications, we have to clean some of the old issues,
+ as many of them have already been resolved with the latest updates.
+
+ Please make sure to update to the latest Home Assistant version and check
+ if that solves the issue. Let us know if that works for you by adding a
+ comment 👍
+
+ This issue now has been marked as stale and will be closed if no further
+ activity occurs. Thank you for your contributions.
+
+# Comment to post when removing the stale label.
+# unmarkComment: >
+# Your comment here.
+
+# Comment to post when closing a stale Issue or Pull Request.
+# closeComment: >
+# Your comment here.
+
+# Limit the number of actions per hour, from 1-30. Default is 30
+limitPerRun: 30
+
+# Limit to only `issues` or `pulls`
+only: issues
diff --git a/azure-pipelines-netlify.yml b/azure-pipelines-netlify.yml
new file mode 100644
index 0000000000..83cf89dcf9
--- /dev/null
+++ b/azure-pipelines-netlify.yml
@@ -0,0 +1,27 @@
+# https://dev.azure.com/home-assistant
+
+trigger: none
+pr: none
+schedules:
+ - cron: "0 0 * * *"
+ displayName: "build preview"
+ branches:
+ include:
+ - dev
+ always: false
+variables:
+ - group: netlify
+
+jobs:
+
+- job: 'Netlify_preview'
+ pool:
+ vmImage: 'ubuntu-latest'
+ steps:
+ - script: |
+ # Cast
+ curl -X POST -d {} https://api.netlify.com/build_hooks/${NETLIFY_CAST}
+
+ # Demo
+ curl -X POST -d {} https://api.netlify.com/build_hooks/${NETLIFY_DEMO}
+ displayName: 'Trigger netlify build preview'
diff --git a/build-scripts/webpack.js b/build-scripts/webpack.js
index d3c57b3c2a..042d978461 100644
--- a/build-scripts/webpack.js
+++ b/build-scripts/webpack.js
@@ -22,7 +22,11 @@ const createWebpackConfig = ({
isProdBuild,
latestBuild,
isStatsBuild,
+ dontHash,
}) => {
+ if (!dontHash) {
+ dontHash = new Set();
+ }
return {
mode: isProdBuild ? "production" : "development",
devtool: isProdBuild ? "source-map" : "inline-cheap-module-source-map",
@@ -103,8 +107,6 @@ const createWebpackConfig = ({
},
output: {
filename: ({ chunk }) => {
- const dontHash = new Set();
-
if (!isProdBuild || dontHash.has(chunk.name)) {
return `${chunk.name}.js`;
}
@@ -222,11 +224,12 @@ const createHassioConfig = ({ isProdBuild, latestBuild }) => {
}
const config = createWebpackConfig({
entry: {
- entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.js"),
+ entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"),
},
outputRoot: "",
isProdBuild,
latestBuild,
+ dontHash: new Set(["entrypoint"]),
});
config.output.path = paths.hassio_root;
diff --git a/cast/src/launcher/layout/hc-cast.ts b/cast/src/launcher/layout/hc-cast.ts
index a63ad71787..859b2cb911 100644
--- a/cast/src/launcher/layout/hc-cast.ts
+++ b/cast/src/launcher/layout/hc-cast.ts
@@ -39,7 +39,7 @@ class HcCast extends LitElement {
@property() private askWrite = false;
@property() private lovelaceConfig?: LovelaceConfig | null;
- protected render(): TemplateResult | void {
+ protected render(): TemplateResult {
if (this.lovelaceConfig === undefined) {
return html`
>
diff --git a/cast/src/launcher/layout/hc-connect.ts b/cast/src/launcher/layout/hc-connect.ts
index a2a56403d1..a2ab3fb179 100644
--- a/cast/src/launcher/layout/hc-connect.ts
+++ b/cast/src/launcher/layout/hc-connect.ts
@@ -70,7 +70,7 @@ export class HcConnect extends LitElement {
@property() private castManager?: CastManager | null;
private openDemo = false;
- protected render(): TemplateResult | void {
+ protected render(): TemplateResult {
if (this.cannotConnect) {
const tokens = loadTokens();
return html`
diff --git a/cast/src/launcher/layout/hc-layout.ts b/cast/src/launcher/layout/hc-layout.ts
index 5decc615a4..aaf9d8c17a 100644
--- a/cast/src/launcher/layout/hc-layout.ts
+++ b/cast/src/launcher/layout/hc-layout.ts
@@ -22,7 +22,7 @@ class HcLayout extends LitElement {
@property() public connection?: Connection;
@property() public user?: HassUser;
- protected render(): TemplateResult | void {
+ protected render(): TemplateResult {
return html`
@@ -50,13 +50,12 @@ class HcLayout extends LitElement {
`;
}
diff --git a/cast/src/receiver/layout/hc-demo.ts b/cast/src/receiver/layout/hc-demo.ts
index 878026e555..4b597d6e1f 100644
--- a/cast/src/receiver/layout/hc-demo.ts
+++ b/cast/src/receiver/layout/hc-demo.ts
@@ -16,7 +16,7 @@ class HcDemo extends HassElement {
@property() public lovelacePath!: string;
@property() private _lovelaceConfig?: LovelaceConfig;
- protected render(): TemplateResult | void {
+ protected render(): TemplateResult {
if (!this._lovelaceConfig) {
return html``;
}
diff --git a/cast/src/receiver/layout/hc-launch-screen.ts b/cast/src/receiver/layout/hc-launch-screen.ts
index a838ae3e7a..ee2586e391 100644
--- a/cast/src/receiver/layout/hc-launch-screen.ts
+++ b/cast/src/receiver/layout/hc-launch-screen.ts
@@ -14,7 +14,7 @@ class HcLaunchScreen extends LitElement {
@property() public hass?: HomeAssistant;
@property() public error?: string;
- protected render(): TemplateResult | void {
+ protected render(): TemplateResult {
return html`
![]()
diff --git a/demo/src/custom-cards/cast-demo-row.ts b/demo/src/custom-cards/cast-demo-row.ts
index fd852f9739..4a19eaf373 100644
--- a/demo/src/custom-cards/cast-demo-row.ts
+++ b/demo/src/custom-cards/cast-demo-row.ts
@@ -10,7 +10,7 @@ import {
import "../../../src/components/ha-icon";
import {
- EntityRow,
+ LovelaceRow,
CastConfig,
} from "../../../src/panels/lovelace/entity-rows/types";
import { HomeAssistant } from "../../../src/types";
@@ -18,7 +18,7 @@ import { CastManager } from "../../../src/cast/cast_manager";
import { castSendShowDemo } from "../../../src/cast/receiver_messages";
@customElement("cast-demo-row")
-class CastDemoRow extends LitElement implements EntityRow {
+class CastDemoRow extends LitElement implements LovelaceRow {
public hass!: HomeAssistant;
@property() private _castManager?: CastManager | null;
@@ -27,7 +27,7 @@ class CastDemoRow extends LitElement implements EntityRow {
// No config possible.
}
- protected render(): TemplateResult | void {
+ protected render(): TemplateResult {
if (
!this._castManager ||
this._castManager.castState === "NO_DEVICES_AVAILABLE"
diff --git a/gallery/src/components/demo-card.js b/gallery/src/components/demo-card.js
index b782aaf882..e52cf02890 100644
--- a/gallery/src/components/demo-card.js
+++ b/gallery/src/components/demo-card.js
@@ -2,7 +2,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { safeLoad } from "js-yaml";
-import { createCardElement } from "../../../src/panels/lovelace/common/create-card-element";
+import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element";
class DemoCard extends PolymerElement {
static get template() {
diff --git a/gallery/src/demos/demo-util-long-press.ts b/gallery/src/demos/demo-util-long-press.ts
index 77cf1e83c0..b1da49043e 100644
--- a/gallery/src/demos/demo-util-long-press.ts
+++ b/gallery/src/demos/demo-util-long-press.ts
@@ -6,7 +6,7 @@ import { actionHandler } from "../../../src/panels/lovelace/common/directives/ac
import { ActionHandlerEvent } from "../../../src/data/lovelace";
export class DemoUtilLongPress extends LitElement {
- protected render(): TemplateResult | void {
+ protected render(): TemplateResult {
return html`
${this.renderStyle()}
${[1, 2, 3].map(
diff --git a/hassio/src/addon-store/hassio-addon-repository.ts b/hassio/src/addon-store/hassio-addon-repository.ts
index 5427ae8fe1..722697ecd2 100644
--- a/hassio/src/addon-store/hassio-addon-repository.ts
+++ b/hassio/src/addon-store/hassio-addon-repository.ts
@@ -15,7 +15,7 @@ import { HomeAssistant } from "../../../src/types";
import {
HassioAddonInfo,
HassioAddonRepository,
-} from "../../../src/data/hassio";
+} from "../../../src/data/hassio/addon";
import { navigate } from "../../../src/common/navigate";
import { filterAndSort } from "../components/hassio-filter-addons";
@@ -36,7 +36,7 @@ class HassioAddonRepositoryEl extends LitElement {
}
);
- protected render(): TemplateResult | void {
+ protected render(): TemplateResult {
const repo = this.repo;
const addons = this._getAddons(this.addons, this.filter);
diff --git a/hassio/src/addon-store/hassio-addon-store.ts b/hassio/src/addon-store/hassio-addon-store.ts
index 2035a5b16f..628ef2bef9 100644
--- a/hassio/src/addon-store/hassio-addon-store.ts
+++ b/hassio/src/addon-store/hassio-addon-store.ts
@@ -14,7 +14,7 @@ import {
HassioAddonInfo,
fetchHassioAddonsInfo,
reloadHassioAddons,
-} from "../../../src/data/hassio";
+} from "../../../src/data/hassio/addon";
import "../../../src/layouts/loading-screen";
import "../components/hassio-search-input";
@@ -48,7 +48,7 @@ class HassioAddonStore extends LitElement {
await this._loadData();
}
- protected render(): TemplateResult | void {
+ protected render(): TemplateResult {
if (!this._addons || !this._repos) {
return html`
diff --git a/hassio/src/addon-store/hassio-repositories-editor.ts b/hassio/src/addon-store/hassio-repositories-editor.ts
index 55832c00ef..5110a77c45 100644
--- a/hassio/src/addon-store/hassio-repositories-editor.ts
+++ b/hassio/src/addon-store/hassio-repositories-editor.ts
@@ -17,7 +17,7 @@ import "../../../src/components/buttons/ha-call-api-button";
import "../components/hassio-card-content";
import { hassioStyle } from "../resources/hassio-style";
import { HomeAssistant } from "../../../src/types";
-import { HassioAddonRepository } from "../../../src/data/hassio";
+import { HassioAddonRepository } from "../../../src/data/hassio/addon";
import { PolymerChangedEvent } from "../../../src/polymer-types";
import { repeat } from "lit-html/directives/repeat";
@@ -33,7 +33,7 @@ class HassioRepositoriesEditor extends LitElement {
.sort((a, b) => (a.name < b.name ? -1 : 1))
);
- protected render(): TemplateResult | void {
+ protected render(): TemplateResult {
const repos = this._sortedRepos(this.repos);
return html`
diff --git a/hassio/src/addon-view/hassio-addon-audio.js b/hassio/src/addon-view/hassio-addon-audio.js
deleted file mode 100644
index 84ec925d80..0000000000
--- a/hassio/src/addon-view/hassio-addon-audio.js
+++ /dev/null
@@ -1,138 +0,0 @@
-import "web-animations-js/web-animations-next-lite.min";
-
-import "@material/mwc-button";
-import "@polymer/paper-card/paper-card";
-import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
-import "@polymer/paper-item/paper-item";
-import "@polymer/paper-listbox/paper-listbox";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-
-import "../../../src/resources/ha-style";
-import { EventsMixin } from "../../../src/mixins/events-mixin";
-
-class HassioAddonAudio extends EventsMixin(PolymerElement) {
- static get template() {
- return html`
-
-
-
-
- [[error]]
-
-
-
-
-
- [[item.name]]
-
-
-
-
-
-
- [[item.name]]
-
-
-
-
-
- Save
-
-
- `;
- }
-
- static get properties() {
- return {
- hass: Object,
- addon: {
- type: Object,
- observer: "addonChanged",
- },
- inputDevices: Array,
- outputDevices: Array,
- selectedInput: String,
- selectedOutput: String,
- error: String,
- };
- }
-
- addonChanged(addon) {
- this.setProperties({
- selectedInput: addon.audio_input || "null",
- selectedOutput: addon.audio_output || "null",
- });
- if (this.outputDevices) return;
-
- const noDevice = [{ device: "null", name: "-" }];
- this.hass.callApi("get", "hassio/hardware/audio").then(
- (resp) => {
- const dev = resp.data.audio;
- const input = Object.keys(dev.input).map((key) => ({
- device: key,
- name: dev.input[key],
- }));
- const output = Object.keys(dev.output).map((key) => ({
- device: key,
- name: dev.output[key],
- }));
- this.setProperties({
- inputDevices: noDevice.concat(input),
- outputDevices: noDevice.concat(output),
- });
- },
- () => {
- this.setProperties({
- inputDevices: noDevice,
- outputDevices: noDevice,
- });
- }
- );
- }
-
- _saveSettings() {
- this.error = null;
- const path = `hassio/addons/${this.addon.slug}/options`;
- this.hass
- .callApi("post", path, {
- audio_input: this.selectedInput === "null" ? null : this.selectedInput,
- audio_output:
- this.selectedOutput === "null" ? null : this.selectedOutput,
- })
- .then(
- () => {
- this.fire("hass-api-called", { success: true, path: path });
- },
- (resp) => {
- this.error = resp.body.message;
- }
- );
- }
-}
-
-customElements.define("hassio-addon-audio", HassioAddonAudio);
diff --git a/hassio/src/addon-view/hassio-addon-audio.ts b/hassio/src/addon-view/hassio-addon-audio.ts
new file mode 100644
index 0000000000..c0718a4c9a
--- /dev/null
+++ b/hassio/src/addon-view/hassio-addon-audio.ts
@@ -0,0 +1,188 @@
+import "web-animations-js/web-animations-next-lite.min";
+
+import "@material/mwc-button";
+import "@polymer/paper-card/paper-card";
+import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
+import "@polymer/paper-item/paper-item";
+import "@polymer/paper-listbox/paper-listbox";
+import {
+ css,
+ CSSResult,
+ customElement,
+ html,
+ LitElement,
+ property,
+ PropertyValues,
+ TemplateResult,
+} from "lit-element";
+
+import { HomeAssistant } from "../../../src/types";
+import {
+ HassioAddonDetails,
+ setHassioAddonOption,
+ HassioAddonSetOptionParams,
+} from "../../../src/data/hassio/addon";
+import {
+ HassioHardwareAudioDevice,
+ fetchHassioHardwareAudio,
+} from "../../../src/data/hassio/hardware";
+import { hassioStyle } from "../resources/hassio-style";
+import { haStyle } from "../../../src/resources/styles";
+
+@customElement("hassio-addon-audio")
+class HassioAddonAudio extends LitElement {
+ @property() public hass!: HomeAssistant;
+ @property() public addon!: HassioAddonDetails;
+ @property() private _error?: string;
+ @property() private _inputDevices?: HassioHardwareAudioDevice[];
+ @property() private _outputDevices?: HassioHardwareAudioDevice[];
+ @property() private _selectedInput!: null | string;
+ @property() private _selectedOutput!: null | string;
+
+ protected render(): TemplateResult {
+ return html`
+
+
+ ${this._error
+ ? html`
+
${this._error}
+ `
+ : ""}
+
+
+
+ ${this._inputDevices &&
+ this._inputDevices.map((item) => {
+ return html`
+ ${item.name}
+ `;
+ })}
+
+
+
+
+ ${this._outputDevices &&
+ this._outputDevices.map((item) => {
+ return html`
+ ${item.name}
+ `;
+ })}
+
+
+
+
+ Save
+
+
+ `;
+ }
+
+ static get styles(): CSSResult[] {
+ return [
+ haStyle,
+ hassioStyle,
+ css`
+ :host,
+ paper-card,
+ paper-dropdown-menu {
+ display: block;
+ }
+ .errors {
+ color: var(--google-red-500);
+ margin-bottom: 16px;
+ }
+ paper-item {
+ width: 450px;
+ }
+ .card-actions {
+ text-align: right;
+ }
+ `,
+ ];
+ }
+
+ protected update(changedProperties: PropertyValues): void {
+ super.update(changedProperties);
+ if (changedProperties.has("addon")) {
+ this._addonChanged();
+ }
+ }
+
+ private _setInputDevice(ev): void {
+ const device = ev.detail.device;
+ if (device) {
+ this._selectedInput = device;
+ }
+ }
+
+ private _setOutputDevice(ev): void {
+ const device = ev.detail.device;
+ if (device) {
+ this._selectedOutput = device;
+ }
+ }
+
+ private async _addonChanged(): Promise
{
+ this._selectedInput = this.addon.audio_input;
+ this._selectedOutput = this.addon.audio_output;
+ if (this._outputDevices) {
+ return;
+ }
+
+ const noDevice: HassioHardwareAudioDevice[] = [
+ { device: undefined, name: "-" },
+ ];
+
+ try {
+ const { audio } = await fetchHassioHardwareAudio(this.hass);
+ const inupt = Object.keys(audio.input).map((key) => ({
+ device: key,
+ name: audio.input[key],
+ }));
+ const output = Object.keys(audio.output).map((key) => ({
+ device: key,
+ name: audio.output[key],
+ }));
+
+ this._inputDevices = noDevice.concat(inupt);
+ this._outputDevices = noDevice.concat(output);
+ } catch {
+ this._error = "Failed to fetch audio hardware";
+ this._inputDevices = noDevice;
+ this._outputDevices = noDevice;
+ }
+ }
+
+ private async _saveSettings(): Promise {
+ this._error = undefined;
+ const data: HassioAddonSetOptionParams = {
+ audio_input: this._selectedInput || null,
+ audio_output: this._selectedOutput || null,
+ };
+ try {
+ await setHassioAddonOption(this.hass, this.addon.slug, data);
+ } catch {
+ this._error = "Failed to set addon audio device";
+ }
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hassio-addon-audio": HassioAddonAudio;
+ }
+}
diff --git a/hassio/src/addon-view/hassio-addon-config.js b/hassio/src/addon-view/hassio-addon-config.js
deleted file mode 100644
index a4061b8899..0000000000
--- a/hassio/src/addon-view/hassio-addon-config.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
-import "@material/mwc-button";
-import "@polymer/paper-card/paper-card";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-
-import "../../../src/components/buttons/ha-call-api-button";
-
-class HassioAddonConfig extends PolymerElement {
- static get template() {
- return html`
-
-
-
-
- Reset to defaults
- Save
-
-
- `;
- }
-
- static get properties() {
- return {
- hass: Object,
- addon: {
- type: Object,
- observer: "addonChanged",
- },
- addonSlug: String,
- config: {
- type: String,
- observer: "configChanged",
- },
- configParsed: Object,
- error: String,
- resetData: {
- type: Object,
- value: {
- options: null,
- },
- },
- };
- }
-
- addonChanged(addon) {
- this.config = addon ? JSON.stringify(addon.options, null, 2) : "";
- }
-
- configChanged(config) {
- try {
- this.$.config.classList.remove("syntaxerror");
- this.configParsed = JSON.parse(config);
- } catch (err) {
- this.$.config.classList.add("syntaxerror");
- this.configParsed = null;
- }
- }
-
- saveTapped() {
- this.error = null;
-
- this.hass
- .callApi("post", `hassio/addons/${this.addonSlug}/options`, {
- options: this.configParsed,
- })
- .catch((resp) => {
- this.error = resp.body.message;
- });
- }
-}
-
-customElements.define("hassio-addon-config", HassioAddonConfig);
diff --git a/hassio/src/addon-view/hassio-addon-config.ts b/hassio/src/addon-view/hassio-addon-config.ts
new file mode 100644
index 0000000000..5d36267a81
--- /dev/null
+++ b/hassio/src/addon-view/hassio-addon-config.ts
@@ -0,0 +1,158 @@
+import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
+import "@material/mwc-button";
+import "@polymer/paper-card/paper-card";
+import {
+ css,
+ CSSResult,
+ customElement,
+ html,
+ LitElement,
+ property,
+ PropertyValues,
+ TemplateResult,
+} from "lit-element";
+
+import { HomeAssistant } from "../../../src/types";
+import {
+ HassioAddonDetails,
+ setHassioAddonOption,
+ HassioAddonSetOptionParams,
+} from "../../../src/data/hassio/addon";
+import { hassioStyle } from "../resources/hassio-style";
+import { haStyle } from "../../../src/resources/styles";
+import { PolymerChangedEvent } from "../../../src/polymer-types";
+import { fireEvent } from "../../../src/common/dom/fire_event";
+
+@customElement("hassio-addon-config")
+class HassioAddonConfig extends LitElement {
+ @property() public hass!: HomeAssistant;
+ @property() public addon!: HassioAddonDetails;
+ @property() private _error?: string;
+ @property() private _config!: string;
+ @property({ type: Boolean }) private _configHasChanged = false;
+
+ protected render(): TemplateResult {
+ return html`
+
+
+ ${this._error
+ ? html`
+
${this._error}
+ `
+ : ""}
+
+
+
+
+ Reset to defaults
+
+
+ Save
+
+
+
+ `;
+ }
+
+ static get styles(): CSSResult[] {
+ return [
+ haStyle,
+ hassioStyle,
+ css`
+ :host {
+ display: block;
+ }
+ paper-card {
+ display: block;
+ }
+ .card-actions {
+ display: flex;
+ justify-content: space-between;
+ }
+ .errors {
+ color: var(--google-red-500);
+ margin-bottom: 16px;
+ }
+ iron-autogrow-textarea {
+ width: 100%;
+ font-family: monospace;
+ }
+ .syntaxerror {
+ color: var(--google-red-500);
+ }
+ `,
+ ];
+ }
+
+ protected updated(changedProperties: PropertyValues): void {
+ super.updated(changedProperties);
+ if (changedProperties.has("addon")) {
+ this._config = JSON.stringify(this.addon.options, null, 2);
+ }
+ }
+
+ private _configChanged(ev: PolymerChangedEvent): void {
+ this._config =
+ ev.detail.value || JSON.stringify(this.addon.options, null, 2);
+ this._configHasChanged =
+ this._config !== JSON.stringify(this.addon.options, null, 2);
+ }
+
+ private async _resetTapped(): Promise {
+ this._error = undefined;
+ const data: HassioAddonSetOptionParams = {
+ options: null,
+ };
+ try {
+ await setHassioAddonOption(this.hass, this.addon.slug, data);
+ this._configHasChanged = false;
+ const eventdata = {
+ success: true,
+ response: undefined,
+ path: "options",
+ };
+ fireEvent(this, "hass-api-called", eventdata);
+ } catch (err) {
+ this._error = `Failed to reset addon configuration, ${err.body?.message ||
+ err}`;
+ }
+ }
+
+ private async _saveTapped(): Promise {
+ let data: HassioAddonSetOptionParams;
+ this._error = undefined;
+ try {
+ data = {
+ options: JSON.parse(this._config),
+ };
+ } catch (err) {
+ this._error = err;
+ return;
+ }
+ try {
+ await setHassioAddonOption(this.hass, this.addon.slug, data);
+ this._configHasChanged = false;
+ const eventdata = {
+ success: true,
+ response: undefined,
+ path: "options",
+ };
+ fireEvent(this, "hass-api-called", eventdata);
+ } catch (err) {
+ this._error = `Failed to save addon configuration, ${err.body?.message ||
+ err}`;
+ }
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hassio-addon-config": HassioAddonConfig;
+ }
+}
diff --git a/hassio/src/addon-view/hassio-addon-info.js b/hassio/src/addon-view/hassio-addon-info.js
deleted file mode 100644
index 1ef1386ecb..0000000000
--- a/hassio/src/addon-view/hassio-addon-info.js
+++ /dev/null
@@ -1,624 +0,0 @@
-import "@polymer/iron-icon/iron-icon";
-import "@material/mwc-button";
-import "@polymer/paper-card/paper-card";
-import "@polymer/paper-tooltip/paper-tooltip";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-
-import "../../../src/components/ha-label-badge";
-import "../../../src/components/ha-markdown";
-import "../../../src/components/buttons/ha-call-api-button";
-import "../../../src/components/ha-switch";
-import "../../../src/resources/ha-style";
-import "../components/hassio-card-content";
-
-import { EventsMixin } from "../../../src/mixins/events-mixin";
-import { navigate } from "../../../src/common/navigate";
-import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
-
-const PERMIS_DESC = {
- rating: {
- title: "Add-on Security Rating",
- description:
- "Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
- },
- host_network: {
- title: "Host Network",
- description:
- "Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the add-on full access to the network capabilities of the host machine. This gives the add-on more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the add-on.",
- },
- homeassistant_api: {
- title: "Home Assistant API Access",
- description:
- "This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the add-on as well, which enables an add-on to interact with Home Assistant without the need for additional authentication tokens.",
- },
- full_access: {
- title: "Full Hardware Access",
- description:
- "This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
- },
- hassio_api: {
- title: "Hass.io API Access",
- description:
- "The add-on was given access to the Hass.io API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Hass.io system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
- },
- docker_api: {
- title: "Full Docker Access",
- description:
- "The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
- },
- host_pid: {
- title: "Host Processes Namespace",
- description:
- "Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
- },
- apparmor: {
- title: "AppArmor",
- description:
- "AppArmor ('Application Armor') is a Linux kernel security module that restricts add-ons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAdd-on authors can provide their security profiles, optimized for the add-on, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the add-on.",
- },
- auth_api: {
- title: "Home Assistant Authentication",
- description:
- "An add-on can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.",
- },
- ingress: {
- title: "Ingress",
- description:
- "This add-on is using Ingress to embed its interface securely into Home Assistant.",
- },
-};
-
-class HassioAddonInfo extends EventsMixin(PolymerElement) {
- static get template() {
- return html`
-
-
-
-
-
-
-
- This update is no longer compatible with your system.
-
-
-
-
- Update
-
-
- Changelog
-
-
-
-
-
-
-
-
- Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.
-
-
- Enable Protection mode
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Show in sidebar
-
-
- This option requires Home Assistant 0.92 or later.
-
-
-
-
-
-
- Protection mode
-
-
- Grant the add-on elevated system access.
-
-
-
-
-
-
-
-
-
- Uninstall
-
- Rebuild
-
-
- Restart
- Stop
-
-
- Start
-
-
- Open web UI
-
-
- Open web UI
-
-
-
-
- This add-on is not available on your system.
-
- Install
-
-
-
-
-
-
-
-
-
-
- `;
- }
-
- static get properties() {
- return {
- hass: Object,
- addon: Object,
- addonSlug: String,
- isRunning: { type: Boolean, computed: "computeIsRunning(addon)" },
- };
- }
-
- computeIsRunning(addon) {
- return addon && addon.state === "started";
- }
-
- computeUpdateAvailable(addon) {
- return (
- addon &&
- !addon.detached &&
- addon.version &&
- addon.version !== addon.last_version
- );
- }
-
- computeHassioApi(addon) {
- return (
- addon.hassio_api &&
- (addon.hassio_role === "manager" || addon.hassio_role === "admin")
- );
- }
-
- computeApparmorClassName(apparmor) {
- if (apparmor === "profile") {
- return "green";
- }
- if (apparmor === "disable") {
- return "red";
- }
- return "";
- }
-
- pathWebui(webui) {
- return webui && webui.replace("[HOST]", document.location.hostname);
- }
-
- computeShowWebUI(ingress, webui, isRunning) {
- return !ingress && webui && isRunning;
- }
-
- openIngress() {
- navigate(this, `/hassio/ingress/${this.addon.slug}`);
- }
-
- computeShowIngressUI(ingress, isRunning) {
- return ingress && isRunning;
- }
-
- computeStartOnBoot(state) {
- return state === "auto";
- }
-
- computeSecurityClassName(rating) {
- if (rating > 4) {
- return "green";
- }
- if (rating > 2) {
- return "yellow";
- }
- return "red";
- }
-
- startOnBootToggled() {
- const data = { boot: this.addon.boot === "auto" ? "manual" : "auto" };
- this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data);
- }
-
- autoUpdateToggled() {
- const data = { auto_update: !this.addon.auto_update };
- this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data);
- }
-
- protectionToggled() {
- const data = { protected: !this.addon.protected };
- this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/security`, data);
- this.set("addon.protected", !this.addon.protected);
- }
-
- panelToggled() {
- const data = { ingress_panel: !this.addon.ingress_panel };
- this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data);
- }
-
- showMoreInfo(e) {
- const id = e.target.getAttribute("id");
- showHassioMarkdownDialog(this, {
- title: PERMIS_DESC[id].title,
- content: PERMIS_DESC[id].description,
- });
- }
-
- openChangelog() {
- this.hass
- .callApi("get", `hassio/addons/${this.addonSlug}/changelog`)
- .then(
- (resp) => resp,
- () => "Error getting changelog"
- )
- .then((content) => {
- showHassioMarkdownDialog(this, {
- title: "Changelog",
- content: content,
- });
- });
- }
-
- _unistallClicked() {
- if (!confirm("Are you sure you want to uninstall this add-on?")) {
- return;
- }
- const path = `hassio/addons/${this.addonSlug}/uninstall`;
- const eventData = {
- path: path,
- };
- this.hass
- .callApi("post", path)
- .then(
- (resp) => {
- eventData.success = true;
- eventData.response = resp;
- },
- (resp) => {
- eventData.success = false;
- eventData.response = resp;
- }
- )
- .then(() => {
- this.fire("hass-api-called", eventData);
- });
- }
-
- _computeCannotIngressSidebar(hass, addon) {
- return !addon.ingress || !this._computeHA92plus(hass);
- }
-
- _computeUsesProtectedOptions(addon) {
- return addon.docker_api || addon.full_access || addon.host_pid;
- }
-
- _computeHA92plus(hass) {
- const [major, minor] = hass.config.version.split(".", 2);
- return Number(major) > 0 || (major === "0" && Number(minor) >= 92);
- }
-}
-customElements.define("hassio-addon-info", HassioAddonInfo);
diff --git a/hassio/src/addon-view/hassio-addon-info.ts b/hassio/src/addon-view/hassio-addon-info.ts
new file mode 100644
index 0000000000..c31de13b80
--- /dev/null
+++ b/hassio/src/addon-view/hassio-addon-info.ts
@@ -0,0 +1,796 @@
+import "@material/mwc-button";
+import "@polymer/iron-icon/iron-icon";
+import "@polymer/paper-card/paper-card";
+import "@polymer/paper-tooltip/paper-tooltip";
+import {
+ css,
+ CSSResult,
+ customElement,
+ html,
+ LitElement,
+ property,
+ TemplateResult,
+} from "lit-element";
+import { classMap } from "lit-html/directives/class-map";
+
+import "../../../src/components/buttons/ha-call-api-button";
+import "../../../src/components/buttons/ha-progress-button";
+import "../../../src/components/ha-label-badge";
+import "../../../src/components/ha-markdown";
+import "../../../src/components/ha-switch";
+import "../components/hassio-card-content";
+
+import { fireEvent } from "../../../src/common/dom/fire_event";
+import {
+ HassioAddonDetails,
+ HassioAddonSetOptionParams,
+ HassioAddonSetSecurityParams,
+ setHassioAddonOption,
+ setHassioAddonSecurity,
+ uninstallHassioAddon,
+ installHassioAddon,
+ fetchHassioAddonChangelog,
+} from "../../../src/data/hassio/addon";
+import { hassioStyle } from "../resources/hassio-style";
+import { haStyle } from "../../../src/resources/styles";
+import { HomeAssistant } from "../../../src/types";
+import { navigate } from "../../../src/common/navigate";
+import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
+
+const PERMIS_DESC = {
+ rating: {
+ title: "Add-on Security Rating",
+ description:
+ "Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
+ },
+ host_network: {
+ title: "Host Network",
+ description:
+ "Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the add-on full access to the network capabilities of the host machine. This gives the add-on more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the add-on.",
+ },
+ homeassistant_api: {
+ title: "Home Assistant API Access",
+ description:
+ "This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the add-on as well, which enables an add-on to interact with Home Assistant without the need for additional authentication tokens.",
+ },
+ full_access: {
+ title: "Full Hardware Access",
+ description:
+ "This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
+ },
+ hassio_api: {
+ title: "Hass.io API Access",
+ description:
+ "The add-on was given access to the Hass.io API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Hass.io system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
+ },
+ docker_api: {
+ title: "Full Docker Access",
+ description:
+ "The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
+ },
+ host_pid: {
+ title: "Host Processes Namespace",
+ description:
+ "Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
+ },
+ apparmor: {
+ title: "AppArmor",
+ description:
+ "AppArmor ('Application Armor') is a Linux kernel security module that restricts add-ons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAdd-on authors can provide their security profiles, optimized for the add-on, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the add-on.",
+ },
+ auth_api: {
+ title: "Home Assistant Authentication",
+ description:
+ "An add-on can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.",
+ },
+ ingress: {
+ title: "Ingress",
+ description:
+ "This add-on is using Ingress to embed its interface securely into Home Assistant.",
+ },
+};
+
+@customElement("hassio-addon-info")
+class HassioAddonInfo extends LitElement {
+ @property() public hass!: HomeAssistant;
+ @property() public addon!: HassioAddonDetails;
+ @property() private _error?: string;
+ @property({ type: Boolean }) private _installing = false;
+
+ protected render(): TemplateResult {
+ return html`
+ ${this._computeUpdateAvailable
+ ? html`
+
+
+
+ ${!this.addon.available
+ ? html`
+
+ This update is no longer compatible with your system.
+
+ `
+ : ""}
+
+
+
+ Update
+
+ ${this.addon.changelog
+ ? html`
+
+ Changelog
+
+ `
+ : ""}
+
+
+ `
+ : ""}
+ ${!this.addon.protected
+ ? html`
+
+
+ Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.
+
+
+ Enable Protection mode
+
+
+
+ `
+ : ""}
+
+