mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-13 13:00:24 +00:00
Compare commits
16 Commits
copilot/fi
...
confdig-se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
359ddbf284 | ||
|
|
49725b4fd3 | ||
|
|
74183b93e7 | ||
|
|
ac711e8abc | ||
|
|
c2cace0dd8 | ||
|
|
2594fcbe33 | ||
|
|
96d220468e | ||
|
|
7a73cf444e | ||
|
|
220fae44b9 | ||
|
|
9e60e36a7e | ||
|
|
05cc3d71aa | ||
|
|
5b031f46f2 | ||
|
|
ec2e3bcada | ||
|
|
3f631cc34a | ||
|
|
3ff8211a81 | ||
|
|
3ebdd11f4b |
@@ -11,11 +11,18 @@ import "./ha-progress-button";
|
||||
class HaCallServiceButton extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
ha-progress-button {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<ha-progress-button
|
||||
id="progress"
|
||||
progress="[[progress]]"
|
||||
on-click="buttonTapped"
|
||||
tabindex="0"
|
||||
raised="[[raised]]"
|
||||
outlined="[[outlined]]"
|
||||
><slot></slot
|
||||
></ha-progress-button>
|
||||
`;
|
||||
@@ -48,6 +55,16 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
|
||||
confirmation: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
raised: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
outlined: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,15 @@ class HaProgressButton extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public raised = false;
|
||||
|
||||
@property({ type: Boolean }) public outlined = false;
|
||||
|
||||
@query("mwc-button") private _button?: Button;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<mwc-button
|
||||
?raised=${this.raised}
|
||||
?outlined=${this.outlined}
|
||||
.disabled=${this.disabled || this.progress}
|
||||
@click=${this._buttonTapped}
|
||||
>
|
||||
@@ -71,6 +74,7 @@ class HaProgressButton extends LitElement {
|
||||
|
||||
mwc-button {
|
||||
transition: all 1s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
mwc-button.success {
|
||||
|
||||
@@ -43,16 +43,16 @@ class PersonBadge extends LitElement {
|
||||
display: contents;
|
||||
}
|
||||
.picture {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: var(--person-picture-size, 40px);
|
||||
height: var(--person-picture-size, 40px);
|
||||
background-size: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.initials {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
width: 40px;
|
||||
line-height: 40px;
|
||||
width: var(--person-picture-size, 40px);
|
||||
line-height: var(--person-picture-size, 40px);
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
background-color: var(--light-primary-color);
|
||||
|
||||
@@ -58,6 +58,7 @@ export type CloudStatus = CloudStatusBase | CloudStatusLoggedIn;
|
||||
|
||||
export interface SubscriptionInfo {
|
||||
human_description: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export interface CloudWebhook {
|
||||
|
||||
@@ -4,13 +4,13 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { formatDateTime } from "../../../../common/datetime/format_date_time";
|
||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||
import "../../../../components/buttons/ha-call-api-button";
|
||||
import "../../../../components/ha-card";
|
||||
import { fetchCloudSubscriptionInfo } from "../../../../data/cloud";
|
||||
import "../../../../layouts/hass-subpage";
|
||||
import { EventsMixin } from "../../../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../../../mixins/localize-mixin";
|
||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||
import "../../../../styles/polymer-ha-style";
|
||||
import "../../ha-config-section";
|
||||
import "./cloud-alexa-pref";
|
||||
@@ -60,10 +60,32 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.integrations {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.integrations cloud-alexa-pref,
|
||||
.integrations cloud-google-pref {
|
||||
width: calc(50% - 12px);
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.integrations cloud-webhooks {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.integrations.narrow cloud-alexa-pref,
|
||||
.integrations.narrow cloud-google-pref {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<hass-subpage header="[[localize('ui.panel.config.cloud.caption')]]">
|
||||
<div class="content">
|
||||
<ha-config-section is-wide="[[isWide]]">
|
||||
<ha-config-section side-by-side is-wide="[[isWide]]">
|
||||
<span slot="header"
|
||||
>[[localize('ui.panel.config.cloud.caption')]]</span
|
||||
>
|
||||
@@ -109,7 +131,7 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
|
||||
<ha-config-section is-wide="[[isWide]]">
|
||||
<ha-config-section side-by-side is-wide="[[isWide]]">
|
||||
<span slot="header"
|
||||
>[[localize('ui.panel.config.cloud.account.integrations')]]</span
|
||||
>
|
||||
@@ -128,13 +150,14 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<cloud-remote-pref
|
||||
hass="[[hass]]"
|
||||
cloud-status="[[cloudStatus]]"
|
||||
dir="[[_rtlDirection]]"
|
||||
></cloud-remote-pref>
|
||||
|
||||
</ha-config-section>
|
||||
<ha-config-section no-header is-wide="[[isWide]]">
|
||||
<div class$="integrations [[_computeIsNarrow(isWide)]]">
|
||||
<cloud-alexa-pref
|
||||
hass="[[hass]]"
|
||||
cloud-status="[[cloudStatus]]"
|
||||
@@ -152,6 +175,7 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
cloud-status="[[cloudStatus]]"
|
||||
dir="[[_rtlDirection]]"
|
||||
></cloud-webhooks>
|
||||
</div>
|
||||
</ha-config-section>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
@@ -227,6 +251,10 @@ class CloudAccount extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
_computeRTLDirection(hass) {
|
||||
return computeRTLDirection(hass);
|
||||
}
|
||||
|
||||
_computeIsNarrow(isWide) {
|
||||
return isWide ? "" : "narrow";
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("cloud-account", CloudAccount);
|
||||
|
||||
@@ -32,17 +32,17 @@ export class CloudAlexaPref extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
header=${this.hass!.localize(
|
||||
.header=${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.alexa.title"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<div class="switch">
|
||||
<ha-switch
|
||||
.checked=${alexa_enabled}
|
||||
@change=${this._enabledToggleChanged}
|
||||
></ha-switch>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
${this.hass!.localize("ui.panel.config.cloud.account.alexa.info")}
|
||||
<ul>
|
||||
<li>
|
||||
@@ -197,6 +197,15 @@ export class CloudAlexaPref extends LitElement {
|
||||
margin-right: 7px;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
ha-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
}
|
||||
.card-content {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ class CloudLogin extends LocalizeMixin(
|
||||
</style>
|
||||
<hass-subpage header="[[localize('ui.panel.config.cloud.caption')]]">
|
||||
<div class="content">
|
||||
<ha-config-section is-wide="[[isWide]]">
|
||||
<ha-config-section side-by-side is-wide="[[isWide]]">
|
||||
<span slot="header"
|
||||
>[[localize('ui.panel.config.cloud.caption')]]</span
|
||||
>
|
||||
|
||||
@@ -49,7 +49,7 @@ class CloudRegister extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
</style>
|
||||
<hass-subpage header="[[localize('ui.panel.config.cloud.register.title')]]">
|
||||
<div class="content">
|
||||
<ha-config-section is-wide="[[isWide]]">
|
||||
<ha-config-section side-by-side is-wide="[[isWide]]">
|
||||
<span slot="header">[[localize('ui.panel.config.cloud.register.headline')]]</span>
|
||||
<div slot="introduction">
|
||||
<p>
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "../../../components/ha-icon-button";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import "../../../styles/polymer-ha-style";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "./ha-config-section-core";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaConfigCore extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.border {
|
||||
margin: 32px auto 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
max-width: 1040px;
|
||||
}
|
||||
|
||||
.narrow .border {
|
||||
max-width: 640px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<hass-tabs-subpage
|
||||
hass="[[hass]]"
|
||||
narrow="[[narrow]]"
|
||||
route="[[route]]"
|
||||
back-path="/config"
|
||||
tabs="[[_computeTabs()]]"
|
||||
show-advanced="[[showAdvanced]]"
|
||||
>
|
||||
<div class$="[[computeClasses(isWide)]]">
|
||||
<ha-config-section-core
|
||||
is-wide="[[isWide]]"
|
||||
show-advanced="[[showAdvanced]]"
|
||||
hass="[[hass]]"
|
||||
></ha-config-section-core>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
isWide: Boolean,
|
||||
narrow: Boolean,
|
||||
showAdvanced: Boolean,
|
||||
route: Object,
|
||||
};
|
||||
}
|
||||
|
||||
_computeTabs() {
|
||||
return configSections.general;
|
||||
}
|
||||
|
||||
computeClasses(isWide) {
|
||||
return isWide ? "content" : "content narrow";
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-config-core", HaConfigCore);
|
||||
66
src/panels/config/core/ha-config-core.ts
Normal file
66
src/panels/config/core/ha-config-core.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
LitElement,
|
||||
CSSResult,
|
||||
css,
|
||||
TemplateResult,
|
||||
html,
|
||||
property,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import "./ha-config-section-core";
|
||||
|
||||
@customElement("ha-config-core")
|
||||
export class HaConfigCore extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public isWide!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public showAdvanced!: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config"
|
||||
.tabs=${configSections.general}
|
||||
show-advanced=${this.showAdvanced}
|
||||
>
|
||||
<ha-config-section-core
|
||||
.isWide=${this.isWide}
|
||||
.narrow=${this.narrow}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.hass=${this.hass}
|
||||
></ha-config-section-core>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
computeClasses(isWide) {
|
||||
return isWide ? "content" : "content narrow";
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-config-section-core {
|
||||
display: block;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-core": HaConfigCore;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
CSSResult,
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "../../../components/ha-card";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||
@@ -87,6 +89,21 @@ class ConfigNameForm extends LitElement {
|
||||
this._working = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-card";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import "../../../styles/polymer-ha-style";
|
||||
import "../ha-config-section";
|
||||
import "./ha-config-core-form";
|
||||
import "./ha-config-name-form";
|
||||
import "./ha-config-url-form";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaConfigSectionCore extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
.validate-container {
|
||||
@apply --layout-vertical;
|
||||
@apply --layout-center-center;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.validate-result {
|
||||
color: var(--success-color);
|
||||
font-weight: 500;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.config-invalid {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.config-invalid .text {
|
||||
color: var(--error-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.config-invalid mwc-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.validate-log {
|
||||
white-space: pre-wrap;
|
||||
direction: ltr;
|
||||
}
|
||||
</style>
|
||||
<ha-config-section is-wide="[[isWide]]">
|
||||
<span slot="header"
|
||||
>[[localize('ui.panel.config.core.section.core.header')]]</span
|
||||
>
|
||||
<span slot="introduction"
|
||||
>[[localize('ui.panel.config.core.section.core.introduction')]]</span
|
||||
>
|
||||
|
||||
<ha-config-name-form hass="[[hass]]"></ha-config-name-form>
|
||||
<ha-config-core-form hass="[[hass]]"></ha-config-core-form>
|
||||
<ha-config-url-form hass="[[hass]]"></ha-config-url-form>
|
||||
</ha-config-section>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
isWide: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
validating: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
isValid: {
|
||||
type: Boolean,
|
||||
value: null,
|
||||
},
|
||||
|
||||
validateLog: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
|
||||
showAdvanced: Boolean,
|
||||
};
|
||||
}
|
||||
|
||||
groupLoaded(hass) {
|
||||
return isComponentLoaded(hass, "group");
|
||||
}
|
||||
|
||||
automationLoaded(hass) {
|
||||
return isComponentLoaded(hass, "automation");
|
||||
}
|
||||
|
||||
scriptLoaded(hass) {
|
||||
return isComponentLoaded(hass, "script");
|
||||
}
|
||||
|
||||
validateConfig() {
|
||||
this.validating = true;
|
||||
this.validateLog = "";
|
||||
this.isValid = null;
|
||||
|
||||
this.hass.callApi("POST", "config/core/check_config").then((result) => {
|
||||
this.validating = false;
|
||||
this.isValid = result.result === "valid";
|
||||
|
||||
if (!this.isValid) {
|
||||
this.validateLog = result.errors;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-config-section-core", HaConfigSectionCore);
|
||||
75
src/panels/config/core/ha-config-section-core.ts
Normal file
75
src/panels/config/core/ha-config-section-core.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../ha-config-section";
|
||||
import "./ha-config-core-form";
|
||||
import "./ha-config-name-form";
|
||||
import "./ha-config-url-form";
|
||||
|
||||
@customElement("ha-config-section-core")
|
||||
export class HaConfigSectionCore extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public isWide!: boolean;
|
||||
|
||||
@property({ type: Boolean, attribute: "narrow", reflect: true })
|
||||
public narrow!: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<div slot="header">
|
||||
${this.hass.localize("ui.panel.config.core.section.core.header")}
|
||||
</div>
|
||||
<div slot="introduction">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.introduction"
|
||||
)}
|
||||
</div>
|
||||
<div class="content">
|
||||
<ha-config-name-form .hass=${this.hass}></ha-config-name-form>
|
||||
<ha-config-url-form .hass=${this.hass}></ha-config-url-form>
|
||||
<ha-config-core-form .hass=${this.hass}></ha-config-core-form>
|
||||
</div>
|
||||
</ha-config-section>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
ha-config-name-form,
|
||||
ha-config-url-form {
|
||||
width: calc(50% - 12px);
|
||||
}
|
||||
|
||||
:host([narrow]) ha-config-url-form,
|
||||
ha-config-core-form {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host([narrow]) ha-config-name-form {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-section-core": HaConfigSectionCore;
|
||||
}
|
||||
}
|
||||
@@ -51,12 +51,6 @@ class ConfigUrlForm extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : ""}
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.external_url"
|
||||
)}
|
||||
</div>
|
||||
|
||||
<paper-input
|
||||
class="flex"
|
||||
@@ -70,14 +64,6 @@ class ConfigUrlForm extends LitElement {
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
</paper-input>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="flex">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.internal_url"
|
||||
)}
|
||||
</div>
|
||||
<paper-input
|
||||
class="flex"
|
||||
.label=${this.hass.localize(
|
||||
@@ -91,7 +77,6 @@ class ConfigUrlForm extends LitElement {
|
||||
>
|
||||
</paper-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._save} .disabled=${disabled}>
|
||||
${this.hass.localize(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,16 +3,24 @@ import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
@customElement("ha-config-section")
|
||||
export class HaConfigSection extends LitElement {
|
||||
@property() public isWide = false;
|
||||
@property({ type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow?: boolean;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-header" }) public noHeader = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
class="content ${classMap({
|
||||
narrow: !this.isWide,
|
||||
narrow: this.narrow !== undefined ? this.narrow : !this.isWide,
|
||||
"no-header": this.noHeader,
|
||||
})}"
|
||||
>
|
||||
<div class="heading">
|
||||
<div class="header"><slot name="header"></slot></div>
|
||||
<div class="intro"><slot name="introduction"></slot></div>
|
||||
</div>
|
||||
<div
|
||||
class="together layout ${classMap({
|
||||
narrow: !this.isWide,
|
||||
@@ -20,7 +28,6 @@ export class HaConfigSection extends LitElement {
|
||||
horizontal: this.isWide,
|
||||
})}"
|
||||
>
|
||||
<div class="intro"><slot name="introduction"></slot></div>
|
||||
<div class="panel flex-auto"><slot></slot></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,10 +45,18 @@ export class HaConfigSection extends LitElement {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
:host([side-by-side]) .content:not(.narrow) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
:host([side-by-side]) .content:not(.narrow) .layout {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -54,7 +69,13 @@ export class HaConfigSection extends LitElement {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
:host([side-by-side]) .content:not(.narrow) .heading {
|
||||
min-width: 400px;
|
||||
max-width: 400px;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
slot[name="header"]::slotted(*) {
|
||||
font-family: var(--paper-font-headline_-_font-family);
|
||||
-webkit-font-smoothing: var(
|
||||
--paper-font-headline_-_-webkit-font-smoothing
|
||||
@@ -64,13 +85,14 @@ export class HaConfigSection extends LitElement {
|
||||
letter-spacing: var(--paper-font-headline_-_letter-spacing);
|
||||
line-height: var(--paper-font-headline_-_line-height);
|
||||
opacity: var(--dark-primary-opacity);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.together {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
slot[name="introduction"]::slotted(*) {
|
||||
font-family: var(--paper-font-subhead_-_font-family);
|
||||
-webkit-font-smoothing: var(
|
||||
--paper-font-subhead_-_-webkit-font-smoothing
|
||||
@@ -78,7 +100,6 @@ export class HaConfigSection extends LitElement {
|
||||
font-weight: var(--paper-font-subhead_-_font-weight);
|
||||
line-height: var(--paper-font-subhead_-_line-height);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin-right: 40px;
|
||||
opacity: var(--dark-primary-opacity);
|
||||
font-size: 14px;
|
||||
@@ -86,7 +107,7 @@ export class HaConfigSection extends LitElement {
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin-top: -24px;
|
||||
margin-top: -48px;
|
||||
}
|
||||
|
||||
.panel ::slotted(*) {
|
||||
@@ -100,11 +121,21 @@ export class HaConfigSection extends LitElement {
|
||||
.narrow .together {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.narrow .intro {
|
||||
.narrow slot[name="introduction"]::slotted(*) {
|
||||
padding-bottom: 20px;
|
||||
margin-right: 0;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.no-header.content {
|
||||
padding-top: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-section": HaConfigSection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
import {
|
||||
mdiAccount,
|
||||
mdiBadgeAccountHorizontal,
|
||||
mdiDevices,
|
||||
mdiHomeAssistant,
|
||||
mdiInformation,
|
||||
mdiMapMarkerRadius,
|
||||
mdiMathLog,
|
||||
mdiNfcVariant,
|
||||
mdiPalette,
|
||||
mdiPencil,
|
||||
mdiPuzzle,
|
||||
mdiRobot,
|
||||
mdiScriptText,
|
||||
mdiServer,
|
||||
mdiShape,
|
||||
mdiSofa,
|
||||
mdiTools,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { PolymerElement } from "@polymer/polymer";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
internalProperty,
|
||||
property,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
@@ -14,26 +34,6 @@ import "../../layouts/hass-loading-screen";
|
||||
import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
|
||||
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
import { HomeAssistant, Route } from "../../types";
|
||||
import {
|
||||
mdiPuzzle,
|
||||
mdiDevices,
|
||||
mdiShape,
|
||||
mdiSofa,
|
||||
mdiRobot,
|
||||
mdiPalette,
|
||||
mdiScriptText,
|
||||
mdiTools,
|
||||
mdiViewDashboard,
|
||||
mdiAccount,
|
||||
mdiMapMarkerRadius,
|
||||
mdiBadgeAccountHorizontal,
|
||||
mdiHomeAssistant,
|
||||
mdiServer,
|
||||
mdiInformation,
|
||||
mdiMathLog,
|
||||
mdiPencil,
|
||||
mdiNfcVariant,
|
||||
} from "@mdi/js";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -167,8 +167,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
iconPath: mdiInformation,
|
||||
core: true,
|
||||
},
|
||||
],
|
||||
advanced: [
|
||||
{
|
||||
component: "customize",
|
||||
path: "/config/customize",
|
||||
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "./integrations-card";
|
||||
import "./system-health-card";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
|
||||
const JS_TYPE = __BUILD__;
|
||||
@@ -94,10 +94,10 @@ class HaConfigInfo extends LitElement {
|
||||
>Python 3</a
|
||||
>,
|
||||
<a
|
||||
href="https://www.polymer-project.org"
|
||||
href="https://lit-element.polymer-project.org/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Polymer</a
|
||||
>LitElement</a
|
||||
>, ${this.hass.localize("ui.panel.config.info.icons_by")}
|
||||
<a
|
||||
href="https://www.google.com/design/icons/"
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import "../../../components/ha-icon-button";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/buttons/ha-progress-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-icon-button";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import {
|
||||
fetchSystemLog,
|
||||
@@ -164,6 +164,10 @@ export class SystemLogCard extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { isServiceLoaded } from "../../../common/config/is_service_loaded";
|
||||
import { componentsWithService } from "../../../common/config/components_with_service";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-card";
|
||||
@@ -27,13 +29,14 @@ import { configSections } from "../ha-panel-config";
|
||||
export class HaConfigServerControl extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public isWide!: boolean;
|
||||
@property({ type: Boolean }) public isWide!: boolean;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
@property({ type: Boolean, attribute: "narrow", reflect: true })
|
||||
public narrow!: boolean;
|
||||
|
||||
@property() public route!: Route;
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property() public showAdvanced!: boolean;
|
||||
@property({ type: Boolean }) public showAdvanced!: boolean;
|
||||
|
||||
@internalProperty() private _validating = false;
|
||||
|
||||
@@ -59,27 +62,29 @@ export class HaConfigServerControl extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
back-path="/config"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config"
|
||||
.tabs=${configSections.general}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
>
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<span slot="header"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.server_control.caption"
|
||||
)}</span
|
||||
<ha-config-section
|
||||
?side-by-side=${!this.showAdvanced}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
>
|
||||
<span slot="introduction"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.server_control.description"
|
||||
)}</span
|
||||
>
|
||||
|
||||
<div slot="header">
|
||||
${this.hass.localize("ui.panel.config.server_control.caption")}
|
||||
</div>
|
||||
<div slot="introduction">
|
||||
${this.hass.localize("ui.panel.config.server_control.description")}
|
||||
</div>
|
||||
<div class="content">
|
||||
${this.showAdvanced
|
||||
? html` <ha-card
|
||||
? html`
|
||||
<ha-card
|
||||
class="validate-card"
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.validation.heading"
|
||||
)}
|
||||
@@ -139,10 +144,14 @@ export class HaConfigServerControl extends LitElement {
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</ha-card>`
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-card
|
||||
class="server-management-card ${classMap({
|
||||
"no-advanced": !this.showAdvanced,
|
||||
})}"
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.heading"
|
||||
)}
|
||||
@@ -152,20 +161,25 @@ export class HaConfigServerControl extends LitElement {
|
||||
"ui.panel.config.server_control.section.server_management.introduction"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-actions warning">
|
||||
<div
|
||||
class="server-management-container layout horizontal center-center warning"
|
||||
>
|
||||
<ha-call-service-button
|
||||
raised
|
||||
class="warning"
|
||||
.hass=${this.hass}
|
||||
domain="homeassistant"
|
||||
service="restart"
|
||||
domain="homeassistant"
|
||||
.hass=${this.hass}
|
||||
.confirmation=${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.confirm_restart"
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.restart"
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
<ha-call-service-button
|
||||
raised
|
||||
class="warning"
|
||||
.hass=${this.hass}
|
||||
domain="homeassistant"
|
||||
@@ -173,16 +187,25 @@ export class HaConfigServerControl extends LitElement {
|
||||
confirmation=${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.confirm_stop"
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.stop"
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
</div>
|
||||
</ha-config-section>
|
||||
<ha-config-section
|
||||
no-header
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
>
|
||||
<div class="content">
|
||||
${this.showAdvanced
|
||||
? html`
|
||||
<ha-card
|
||||
class="reload"
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.reloading.heading"
|
||||
)}
|
||||
@@ -192,37 +215,38 @@ export class HaConfigServerControl extends LitElement {
|
||||
"ui.panel.config.server_control.section.reloading.introduction"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<div class="actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
outlined
|
||||
domain="homeassistant"
|
||||
service="reload_core_config"
|
||||
>${this.hass.localize(
|
||||
.hass=${this.hass}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.reloading.core"
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
${this._reloadableDomains.map(
|
||||
(domain) =>
|
||||
html`<div class="card-actions">
|
||||
${this._reloadableDomains.map((domain) =>
|
||||
isServiceLoaded(this.hass, domain, "reload")
|
||||
? html`
|
||||
<ha-call-service-button
|
||||
outlined
|
||||
service="reload"
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
service="reload"
|
||||
>${this.hass.localize(
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.server_control.section.reloading.${domain}`
|
||||
) ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.server_control.section.reloading.reload",
|
||||
"domain",
|
||||
domainToName(this.hass.localize, domain)
|
||||
)}
|
||||
) || domainToName(this.hass.localize, domain)}
|
||||
</ha-call-service-button>
|
||||
</div>`
|
||||
`
|
||||
: ""
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</ha-config-section>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
@@ -246,10 +270,48 @@ export class HaConfigServerControl extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.validate-container {
|
||||
.heading {
|
||||
max-width: 1040px;
|
||||
margin: 0px auto;
|
||||
}
|
||||
|
||||
.heading {
|
||||
padding: 28px 20px 0px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.validate-card,
|
||||
.server-management-card {
|
||||
width: calc(50% - 12px);
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
padding-bottom: 8px;
|
||||
opacity: var(--dark-primary-opacity);
|
||||
}
|
||||
|
||||
.description {
|
||||
opacity: var(--dark-primary-opacity);
|
||||
font-size: 14px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.validate-container,
|
||||
.server-management-container {
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.server-management-container ha-call-service-button {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.validate-result {
|
||||
color: var(--success-color);
|
||||
font-weight: 500;
|
||||
@@ -274,10 +336,50 @@ export class HaConfigServerControl extends LitElement {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
ha-config-section {
|
||||
padding-bottom: 24px;
|
||||
.warning {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
|
||||
.reload {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.reload .actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.reload ha-call-service-button {
|
||||
padding: 0 8px;
|
||||
display: inline-block;
|
||||
width: calc(33% - 24px);
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
:host([narrow]) .validate-card,
|
||||
:host([narrow]) .server-management-card,
|
||||
.server-management-card.no-advanced {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host([narrow]) .server-management-card {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
:host([narrow]) .reload ha-call-service-button {
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
border: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-server-control": HaConfigServerControl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -866,38 +866,37 @@
|
||||
},
|
||||
"reloading": {
|
||||
"heading": "YAML configuration reloading",
|
||||
"introduction": "Some parts of Home Assistant can reload without requiring a restart. Hitting reload will unload their current YAML configuration and load the new one.",
|
||||
"reload": "Reload {domain}",
|
||||
"core": "Reload location & customizations",
|
||||
"group": "Reload groups, group entities, and notify services",
|
||||
"automation": "Reload automations",
|
||||
"script": "Reload scripts",
|
||||
"scene": "Reload scenes",
|
||||
"person": "Reload persons",
|
||||
"zone": "Reload zones",
|
||||
"input_boolean": "Reload input booleans",
|
||||
"input_text": "Reload input texts",
|
||||
"input_number": "Reload input numbers",
|
||||
"input_datetime": "Reload input date times",
|
||||
"input_select": "Reload input selects",
|
||||
"template": "Reload template entities",
|
||||
"universal": "Reload universal media player entities",
|
||||
"rest": "Reload rest entities and notify services",
|
||||
"command_line": "Reload command line entities",
|
||||
"filter": "Reload filter entities",
|
||||
"statistics": "Reload statistics entities",
|
||||
"generic": "Reload generic IP camera entities",
|
||||
"generic_thermostat": "Reload generic thermostat entities",
|
||||
"homekit": "Reload HomeKit",
|
||||
"min_max": "Reload min/max entities",
|
||||
"history_stats": "Reload history stats entities",
|
||||
"trend": "Reload trend entities",
|
||||
"ping": "Reload ping binary sensor entities",
|
||||
"filesize": "Reload file size entities",
|
||||
"telegram": "Reload telegram notify services",
|
||||
"smtp": "Reload smtp notify services",
|
||||
"mqtt": "Reload mqtt entities",
|
||||
"rpi_gpio": "Reload Raspberry Pi GPIO entities"
|
||||
"introduction": "Some parts of Home Assistant can reload without requiring a restart. Clicking a button below will unload their current YAML configuration and load the new one.",
|
||||
"core": "location & customizations",
|
||||
"group": "groups, group entities, and notify services",
|
||||
"automation": "automations",
|
||||
"script": "scripts",
|
||||
"scene": "scenes",
|
||||
"person": "persons",
|
||||
"zone": "zones",
|
||||
"input_boolean": "input booleans",
|
||||
"input_text": "input texts",
|
||||
"input_number": "input numbers",
|
||||
"input_datetime": "input date times",
|
||||
"input_select": "input selects",
|
||||
"template": "template entities",
|
||||
"universal": "universal media player entities",
|
||||
"rest": "rest entities and notify services",
|
||||
"command_line": "command line entities",
|
||||
"filter": "filter entities",
|
||||
"statistics": "statistics entities",
|
||||
"generic": "generic IP camera entities",
|
||||
"generic_thermostat": "generic thermostat entities",
|
||||
"homekit": "HomeKit",
|
||||
"min_max": "min/max entities",
|
||||
"history_stats": "history stats entities",
|
||||
"trend": "trend entities",
|
||||
"ping": "ping binary sensor entities",
|
||||
"filesize": "file size entities",
|
||||
"telegram": "telegram notify services",
|
||||
"smtp": "smtp notify services",
|
||||
"mqtt": "mqtt entities",
|
||||
"rpi_gpio": "Raspberry Pi GPIO entities"
|
||||
},
|
||||
"server_management": {
|
||||
"heading": "Server management",
|
||||
@@ -1322,6 +1321,8 @@
|
||||
"description_login": "Logged in as {email}",
|
||||
"description_not_login": "Not logged in",
|
||||
"description_features": "Control away from home, integrate with Alexa and Google Assistant.",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"login": {
|
||||
"title": "Cloud Login",
|
||||
"introduction": "Home Assistant Cloud provides you with a secure remote connection to your instance while away from home. It also allows you to connect with cloud-only services: Amazon Alexa and Google Assistant.",
|
||||
|
||||
Reference in New Issue
Block a user