Compare commits

...

63 Commits

Author SHA1 Message Date
Zack Barett
27ca45dc70 Bumped version to 20220420.0 (#12369) 2022-04-20 22:43:40 +00:00
Zack Barett
d290c11219 Config menu updates to get it ready for nightly (#12368) 2022-04-20 22:38:35 +00:00
Zack Barett
cabe10ffdb Getting started on Configuration Changes (#12309)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-04-20 16:57:51 -05:00
Franck Nijhof
aa562c21a8 Add stop script/automation action (#12299) 2022-04-20 16:50:09 -05:00
Franck Nijhof
22175a7271 Add if/else automation/script action (#12301)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-04-20 11:27:16 -05:00
Eric Stern
1e0647c0d1 Github no longer supports the (insecure) git protocol (#12359) 2022-04-20 13:52:12 +00:00
Simon Vallières
58d94da8b3 Adding blueprint input description markdown/multi-line support (#12291) 2022-04-20 08:36:18 -05:00
Joakim Sørensen
d97763a3e8 Add clear skipped to update more-info dialog (#12361) 2022-04-20 08:17:56 -05:00
Paulus Schoutsen
aa129aa123 Bump HAWS to 7.0.3 (#12358) 2022-04-19 12:50:18 -05:00
Franck Nijhof
f648317206 Fix strict error handling in developer tools templates (#12352) 2022-04-19 07:28:07 -05:00
Raman Gupta
0685fdf7c6 Add basic frontend support for siren (#12345) 2022-04-18 11:19:01 -05:00
Franck Nijhof
6fd4cda534 Add Template selector (#12348) 2022-04-18 11:17:44 -05:00
Franck Nijhof
511368da13 Allow selecting multiple entities for state trigger (#12334)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-04-15 19:03:14 +00:00
J. Nick Koston
76e1721c58 Quickly search for entities from the Overview Dashboard (#12324) 2022-04-15 13:54:57 -05:00
Yosi Levy
bad5a389b5 RTL calendar fix - arrows fix and views fix (#12314)
* RTL calendar fix - arrows fix and views fix

* Removed path attributes
2022-04-15 13:47:46 -05:00
Paulus Schoutsen
85d1f49763 Allow tapping on the name on a picture entity card (#12332) 2022-04-15 08:55:22 -05:00
Paulus Schoutsen
7723d47ac1 Split only on first comma in media browser (#12331) 2022-04-14 17:52:49 -05:00
J. Nick Koston
30b130ca74 Use new mdi icons for smoke and co detection (#12323) 2022-04-14 15:42:54 -07:00
Joakim Sørensen
a124ec0717 Always render title field (#12319) 2022-04-13 07:20:00 -05:00
Joakim Sørensen
323d98ecf7 Decode view path URL (#12310) 2022-04-12 06:52:17 -05:00
Paulus Schoutsen
125a601ae3 Select default mode if none set (#12306) 2022-04-11 12:08:37 -05:00
Paulus Schoutsen
3c549c6b31 Update cloud text (#12305) 2022-04-11 09:47:31 -07:00
Kuba Wolanin
9c1494c74d Fix endless loading screen in zwave-js config (#12295) 2022-04-11 16:56:03 +02:00
Philip Allgaier
e751abd775 Prevent empty brackets if no manufacturer during config entry creation (#12288) 2022-04-11 09:06:29 -05:00
Joakim Sørensen
714f2447b7 Use more text selector types for add-on configuration (#12303) 2022-04-11 16:01:30 +02:00
Franck Nijhof
d900e40d04 Fix add-on security rating range (#12300) 2022-04-11 14:04:54 +02:00
Joakim Sørensen
8b82383790 Guard for partial translations (#12296) 2022-04-11 13:03:41 +02:00
Zack Barett
5a2cc2646c Merge pull request #12252 from spacegaier/issue-12246 2022-04-07 16:40:08 -05:00
Philip Allgaier
16a0902989 Adjust import 2022-04-07 22:41:37 +02:00
Philip Allgaier
8f67aa38af Fix entity and device selector with multiple: true 2022-04-07 21:39:04 +02:00
Zack Barett
34184cf2ab Merge pull request #12250 from spacegaier/issue-12248 2022-04-07 14:11:29 -05:00
Philip Allgaier
611cd2818e Exclude hidden entities from area card 2022-04-07 20:58:21 +02:00
Zack Barett
0a4e8fd5d0 Merge pull request #12244 from home-assistant/lineup-badges 2022-04-07 13:19:48 -05:00
Ludeeus
11f0361f48 Lineup sidebar badges 2022-04-07 06:54:24 +00:00
Philip Allgaier
cfa048ea4e Only show "required" indicator if we have a selector label (#12241) 2022-04-06 22:11:12 +00:00
Joakim Sørensen
bbca7b762b Use selectors for add-on network configuration (#12235)
* Use selectors for add-on network configuration

* Show container port as UOM if advanced user

* adjust
2022-04-06 22:21:06 +02:00
Erik Montnemery
1dba849567 Fix statistics chart for sum stat without state (#12238) 2022-04-06 20:54:11 +02:00
Marius
aff1ec10bf replace ToggleSwitch with new LightSwitch (#12218) 2022-04-06 16:26:34 +02:00
Joakim Sørensen
351ec08a71 Use selectors for add-on configurations (#12234) 2022-04-06 09:57:17 +02:00
Paulus Schoutsen
a1a6a2cd30 Bumped version to 20220405.0 2022-04-05 15:49:13 -07:00
Joakim Sørensen
4e82c23b29 Fixes for flow help icon (#12224) 2022-04-05 15:47:24 -07:00
Paulus Schoutsen
59595aabde Add helpers to all selectors (#12230) 2022-04-05 15:26:52 -05:00
Paulus Schoutsen
358f91c2a9 Add statistic name to adjust dialog (#12229) 2022-04-05 17:25:23 +00:00
Joakim Sørensen
e0e01e68b4 Use change instead of click when selecting home assistant in backup (#12226) 2022-04-05 10:48:45 -05:00
Paulus Schoutsen
61dc4eaaea Throttle counting updates (#12223) 2022-04-05 09:55:01 +02:00
Paulus Schoutsen
65c4d02452 Fix Safari dates (#12222) 2022-04-04 21:54:35 -07:00
Philip Allgaier
f78ce2c844 Sort "Switch as" domains and add separator (#12216) 2022-04-04 18:47:26 -05:00
Philip Allgaier
4d1ab83b30 Hide "Show as" separator if there is nothing above/below it (#12219) 2022-04-04 18:44:21 -05:00
Zack Barett
fb4b40b828 Fix Entity Settings missing (#12221) 2022-04-04 22:49:11 +00:00
Philip Allgaier
db0c4ef941 Fix "Show as" in entity registry (#12215) 2022-04-04 14:16:03 -05:00
Philip Allgaier
c5b60b826b Enable <ha-form> overflow overriding via part (#12204) 2022-04-04 08:41:12 -05:00
Philip Allgaier
718f0330a7 Fix button card behavior to toggle scenes (#12203) 2022-04-04 08:40:10 -05:00
Philip Allgaier
89e31486c5 Ensure history entity picker wraps to next line on mobile (#12201) 2022-04-04 08:39:45 -05:00
Philip Allgaier
717eec1860 Allow "none" again as secondary entity information (#12199) 2022-04-04 08:39:28 -05:00
Philip Allgaier
b6e51352e3 Fan more-info: Add margin to sections (#12202) 2022-04-04 08:38:53 -05:00
Joakim Sørensen
2ade728bc3 Make the progress bar not jump the dialog (#12212) 2022-04-04 08:20:37 +02:00
Joakim Sørensen
62f227da83 Use installed_version for update entities (#12194) 2022-04-01 19:28:39 +02:00
Bram Kragten
9557b604da Bumped version to 20220401.0 2022-04-01 18:35:08 +02:00
Zack Barett
b45c355c9f Fix for Mult enabled selectors when required (#12191) 2022-04-01 18:34:32 +02:00
Joakim Sørensen
0b47d2c687 Redirect old backup links to backup integration for non supervised (#12183) 2022-04-01 08:05:53 -07:00
Joakim Sørensen
8baa0b2a9b Hide skip when auto_update is true for updates (#12184) 2022-04-01 14:37:47 +02:00
Joakim Sørensen
c68a1d21ff Do not offer to partially backup homeassistant configuration (#12188) 2022-04-01 14:37:18 +02:00
Paulus Schoutsen
419d659311 Guard calling input select row with bad option (#12181) 2022-03-31 20:32:10 -05:00
119 changed files with 2130 additions and 1005 deletions

View File

@@ -170,6 +170,7 @@ const SCHEMAS: {
select: { options: ["Option 1", "Option 2"], mode: "list" },
},
},
template: { name: "Template", selector: { template: {} } },
select: {
name: "Select",
selector: {
@@ -261,6 +262,8 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
@state() private _required = false;
@state() private _helper = false;
@state() private _label = true;
private data = SCHEMAS.map(() => ({}));
@@ -418,6 +421,13 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
@change=${this._handleOptionChange}
></ha-switch>
</ha-formfield>
<ha-formfield label="Helper text">
<ha-switch
.name=${"helper"}
.checked=${this._helper}
@change=${this._handleOptionChange}
></ha-switch>
</ha-formfield>
</div>
${SCHEMAS.map((info, idx) => {
const data = this.data[idx];
@@ -446,6 +456,7 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
.disabled=${this._disabled}
.required=${this._required}
@value-changed=${valueChanged}
.helper=${this._helper ? "Helper text" : undefined}
></ha-selector>
</ha-settings-row>
`
@@ -466,7 +477,8 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
width: 60;
}
.options {
padding: 16px 48px;
max-width: 800px;
margin: 16px auto;
}
.options ha-formfield {
margin-right: 16px;

View File

@@ -18,7 +18,7 @@ import { LONG_TEXT } from "../../data/text";
const base_attributes = {
title: "Awesome",
current_version: "1.2.2",
installed_version: "1.2.2",
latest_version: "1.2.3",
release_url: "https://home-assistant.io",
supported_features: UPDATE_SUPPORT_INSTALL,
@@ -50,7 +50,7 @@ const ENTITIES = [
}),
getEntity("update", "update5", "off", {
...base_attributes,
current_version: "1.2.3",
installed_version: "1.2.3",
friendly_name: "No update",
}),
getEntity("update", "update6", "off", {
@@ -102,8 +102,8 @@ const ENTITIES = [
}),
getEntity("update", "update14", "off", {
...base_attributes,
current_version: null,
friendly_name: "Update without current_version",
installed_version: null,
friendly_name: "Update without installed_version",
}),
getEntity("update", "update15", "off", {
...base_attributes,
@@ -128,6 +128,17 @@ const ENTITIES = [
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
}),
getEntity("update", "update19", "on", {
...base_attributes,
friendly_name: "Update with auto update",
auto_update: true,
}),
getEntity("update", "update20", "on", {
...base_attributes,
in_progress: true,
title: undefined,
friendly_name: "Installing without title",
}),
];
@customElement("demo-more-info-update")

View File

@@ -39,7 +39,14 @@ import type { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style";
const SUPPORTED_UI_TYPES = ["string", "select", "boolean", "integer", "float"];
const SUPPORTED_UI_TYPES = [
"string",
"select",
"boolean",
"integer",
"float",
"schema",
];
const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
new Type("!secret", {
@@ -48,6 +55,8 @@ const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
}),
]);
const MASKED_FIELDS = ["password", "secret", "token"];
@customElement("hassio-addon-config")
class HassioAddonConfig extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails;
@@ -75,19 +84,66 @@ class HassioAddonConfig extends LitElement {
public computeLabel = (entry: HaFormSchema): string =>
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
?.name ||
this.addon.translations.en?.configuration?.[entry.name].name ||
this.addon.translations.en?.configuration?.[entry.name]?.name ||
entry.name;
private _schema = memoizeOne((schema: HaFormSchema[]): HaFormSchema[] =>
// @ts-expect-error supervisor does not implement [string, string] for select.options[]
schema.map((entry) =>
entry.type === "select"
? {
...entry,
options: entry.options.map((option) => [option, option]),
}
: entry
)
public computeHelper = (entry: HaFormSchema): string =>
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
?.description ||
this.addon.translations.en?.configuration?.[entry.name]?.description ||
"";
private _convertSchema = memoizeOne(
// Convert supervisor schema to selectors
(schema: Record<string, any>): HaFormSchema[] =>
schema.map((entry) =>
entry.type === "select"
? {
name: entry.name,
required: entry.required,
selector: { select: { options: entry.options } },
}
: entry.type === "string"
? entry.multiple
? {
name: entry.name,
required: entry.required,
selector: {
select: { options: [], multiple: true, custom_value: true },
},
}
: {
name: entry.name,
required: entry.required,
selector: {
text: {
type:
entry.format || MASKED_FIELDS.includes(entry.name)
? "password"
: "text",
},
},
}
: entry.type === "boolean"
? {
name: entry.name,
required: entry.required,
selector: { boolean: {} },
}
: entry.type === "schema"
? {
name: entry.name,
required: entry.required,
selector: { object: {} },
}
: entry.type === "float" || entry.type === "integer"
? {
name: entry.name,
required: entry.required,
selector: { number: { mode: "box" } },
}
: entry
)
);
private _filteredShchema = memoizeOne(
@@ -140,7 +196,8 @@ class HassioAddonConfig extends LitElement {
.data=${this._options!}
@value-changed=${this._configChanged}
.computeLabel=${this.computeLabel}
.schema=${this._schema(
.computeHelper=${this.computeHelper}
.schema=${this._convertSchema(
this._showOptional
? this.addon.schema!
: this._filteredShchema(
@@ -197,8 +254,9 @@ class HassioAddonConfig extends LitElement {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._canShowSchema = !this.addon.schema!.find(
// @ts-ignore
(entry) => !SUPPORTED_UI_TYPES.includes(entry.type) || entry.multiple
(entry) =>
// @ts-ignore
!SUPPORTED_UI_TYPES.includes(entry.type)
);
this._yamlMode = !this._canShowSchema;
}

View File

@@ -1,4 +1,3 @@
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
css,
CSSResultGroup,
@@ -8,10 +7,13 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import {
HassioAddonDetails,
HassioAddonSetOptionParams,
@@ -24,16 +26,6 @@ import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style";
interface NetworkItem {
description: string;
container: string;
host: number | null;
}
interface NetworkItemInput extends PaperInputElement {
container: string;
}
@customElement("hassio-addon-network")
class HassioAddonNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -42,9 +34,13 @@ class HassioAddonNetwork extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails;
@state() private _showOptional = false;
@state() private _configHasChanged = false;
@state() private _error?: string;
@state() private _config?: NetworkItem[];
@state() private _config?: Record<string, any>;
public connectedCallback(): void {
super.connectedCallback();
@@ -56,6 +52,10 @@ class HassioAddonNetwork extends LitElement {
return html``;
}
const hasHiddenOptions = Object.keys(this._config).find(
(entry) => this._config![entry] === null
);
return html`
<ha-card
.header=${this.supervisor.localize(
@@ -63,52 +63,49 @@ class HassioAddonNetwork extends LitElement {
)}
>
<div class="card-content">
<p>
${this.supervisor.localize(
"addon.configuration.network.introduction"
)}
</p>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<table>
<tbody>
<tr>
<th>
${this.supervisor.localize(
"addon.configuration.network.container"
)}
</th>
<th>
${this.supervisor.localize(
"addon.configuration.network.host"
)}
</th>
<th>${this.supervisor.localize("common.description")}</th>
</tr>
${this._config!.map(
(item) => html`
<tr>
<td>${item.container}</td>
<td>
<paper-input
@value-changed=${this._configChanged}
placeholder=${this.supervisor.localize(
"addon.configuration.network.disabled"
)}
.value=${item.host ? String(item.host) : ""}
.container=${item.container}
no-label-float
></paper-input>
</td>
<td>${this._computeDescription(item)}</td>
</tr>
`
)}
</tbody>
</table>
<ha-form
.data=${this._config}
@value-changed=${this._configChanged}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
.schema=${this._createSchema(
this._config,
this._showOptional,
this.hass.userData?.showAdvanced || false
)}
></ha-form>
</div>
${hasHiddenOptions
? html`<ha-formfield
class="show-optional"
.label=${this.supervisor.localize(
"addon.configuration.network.show_disabled"
)}
>
<ha-switch
@change=${this._toggleOptional}
.checked=${this._showOptional}
>
</ha-switch>
</ha-formfield>`
: ""}
<div class="card-actions">
<ha-progress-button class="warning" @click=${this._resetTapped}>
${this.supervisor.localize("common.reset_defaults")}
</ha-progress-button>
<ha-progress-button @click=${this._saveTapped}>
<ha-progress-button
@click=${this._saveTapped}
.disabled=${!this._configHasChanged}
>
${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
@@ -123,50 +120,60 @@ class HassioAddonNetwork extends LitElement {
}
}
private _computeDescription = (item: NetworkItem): string =>
this.addon.translations[this.hass.language]?.network?.[item.container]
?.description ||
this.addon.translations.en?.network?.[item.container]?.description ||
item.description;
private _createSchema = memoizeOne(
(
config: Record<string, number>,
showOptional: boolean,
advanced: boolean
): HaFormSchema[] =>
(showOptional
? Object.keys(config)
: Object.keys(config).filter((entry) => config[entry] !== null)
).map((entry) => ({
name: entry,
selector: {
number: {
mode: "box",
min: 0,
max: 65535,
unit_of_measurement: advanced ? entry : undefined,
},
},
}))
);
private _computeLabel = (_: HaFormSchema): string => "";
private _computeHelper = (item: HaFormSchema): string =>
this.addon.translations[this.hass.language]?.network?.[item.name] ||
this.addon.translations.en?.network?.[item.name] ||
this.addon.network_description?.[item.name] ||
item.name;
private _setNetworkConfig(): void {
const network = this.addon.network || {};
const description = this.addon.network_description || {};
const items: NetworkItem[] = Object.keys(network).map((key) => ({
container: key,
host: network[key],
description: description[key],
}));
this._config = items.sort((a, b) => (a.container > b.container ? 1 : -1));
this._config = this.addon.network || {};
}
private async _configChanged(ev: Event): Promise<void> {
const target = ev.target as NetworkItemInput;
this._config!.forEach((item) => {
if (
item.container === target.container &&
item.host !== parseInt(String(target.value), 10)
) {
item.host = target.value ? parseInt(String(target.value), 10) : null;
}
});
private async _configChanged(ev: CustomEvent): Promise<void> {
this._configHasChanged = true;
this._config! = ev.detail.value;
}
private async _resetTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const data: HassioAddonSetOptionParams = {
network: null,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "option",
};
button.actionSuccess();
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
@@ -177,19 +184,21 @@ class HassioAddonNetwork extends LitElement {
"error",
extractApiErrorMessage(err)
);
button.actionError();
}
}
button.progress = false;
private _toggleOptional() {
this._showOptional = !this._showOptional;
}
private async _saveTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
this._error = undefined;
const networkconfiguration = {};
this._config!.forEach((item) => {
networkconfiguration[item.container] = parseInt(String(item.host), 10);
Object.entries(this._config!).forEach(([key, value]) => {
networkconfiguration[key] = value ?? null;
});
const data: HassioAddonSetOptionParams = {
@@ -198,11 +207,13 @@ class HassioAddonNetwork extends LitElement {
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "option",
};
button.actionSuccess();
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
@@ -213,8 +224,8 @@ class HassioAddonNetwork extends LitElement {
"error",
extractApiErrorMessage(err)
);
button.actionError();
}
button.progress = false;
}
static get styles(): CSSResultGroup {
@@ -232,6 +243,9 @@ class HassioAddonNetwork extends LitElement {
display: flex;
justify-content: space-between;
}
.show-optional {
padding: 16px;
}
`,
];
}

View File

@@ -32,13 +32,6 @@ interface AddonCheckboxItem extends CheckboxItem {
const _computeFolders = (folders): CheckboxItem[] => {
const list: CheckboxItem[] = [];
if (folders.includes("homeassistant")) {
list.push({
slug: "homeassistant",
name: "Home Assistant configuration",
checked: false,
});
}
if (folders.includes("ssl")) {
list.push({ slug: "ssl", name: "SSL", checked: false });
}
@@ -100,7 +93,7 @@ export class SupervisorBackupContent extends LitElement {
this.folders = _computeFolders(
this.backup
? this.backup.folders
: ["homeassistant", "ssl", "share", "media", "addons/local"]
: ["ssl", "share", "media", "addons/local"]
);
this.addons = _computeAddons(
this.backup ? this.backup.addons : this.supervisor?.supervisor.addons
@@ -187,7 +180,7 @@ export class SupervisorBackupContent extends LitElement {
>
<ha-checkbox
.checked=${this.homeAssistant}
@click=${this.toggleHomeAssistant}
@change=${this.toggleHomeAssistant}
>
</ha-checkbox>
</ha-formfield>

View File

@@ -108,7 +108,7 @@
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^1.1.5",
"home-assistant-js-websocket": "^7.0.1",
"home-assistant-js-websocket": "^7.0.3",
"idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1",
"js-yaml": "^4.1.0",

View File

@@ -15,7 +15,7 @@ if [ -z $(which hass) ]; then
echo "Installing Home Asstant core from dev."
python3 -m pip install --upgrade \
colorlog \
git+git://github.com/home-assistant/home-assistant.git@dev
git+https://github.com/home-assistant/home-assistant.git@dev
fi
if [ ! -d "${WD}/config" ]; then

View File

@@ -1,6 +1,6 @@
[metadata]
name = home-assistant-frontend
version = 20220330.0
version = 20220420.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0

View File

@@ -12,7 +12,7 @@ export const isNavigationClick = (e: MouseEvent) => {
const anchor = e
.composedPath()
.filter((n) => (n as HTMLElement).tagName === "A")[0] as
.find((n) => (n as HTMLElement).tagName === "A") as
| HTMLAnchorElement
| undefined;
if (

View File

@@ -29,8 +29,11 @@ import {
mdiPowerPlug,
mdiPowerPlugOff,
mdiRadioboxBlank,
mdiSmoke,
mdiSnowflake,
mdiSmokeDetector,
mdiSmokeDetectorAlert,
mdiSmokeDetectorVariant,
mdiSmokeDetectorVariantAlert,
mdiSquare,
mdiSquareOutline,
mdiStop,
@@ -52,6 +55,8 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
return is_off ? mdiBattery : mdiBatteryOutline;
case "battery_charging":
return is_off ? mdiBattery : mdiBatteryCharging;
case "carbon_monoxide":
return is_off ? mdiSmokeDetector : mdiSmokeDetectorAlert;
case "cold":
return is_off ? mdiThermometer : mdiSnowflake;
case "connectivity":
@@ -68,7 +73,7 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
case "tamper":
return is_off ? mdiCheckCircle : mdiAlertCircle;
case "smoke":
return is_off ? mdiCheckCircle : mdiSmoke;
return is_off ? mdiSmokeDetectorVariant : mdiSmokeDetectorVariantAlert;
case "heat":
return is_off ? mdiThermometer : mdiFire;
case "light":

View File

@@ -8,26 +8,25 @@ import {
mdiCalendar,
mdiCast,
mdiCastConnected,
mdiCheckCircleOutline,
mdiClock,
mdiCloseCircleOutline,
mdiGestureTapButton,
mdiLanConnect,
mdiLanDisconnect,
mdiLightSwitch,
mdiLock,
mdiLockAlert,
mdiLockClock,
mdiLockOpen,
mdiPackage,
mdiPackageDown,
mdiPackageUp,
mdiPowerPlug,
mdiPowerPlugOff,
mdiRestart,
mdiToggleSwitch,
mdiToggleSwitchOff,
mdiCheckCircleOutline,
mdiCloseCircleOutline,
mdiToggleSwitchVariant,
mdiToggleSwitchVariantOff,
mdiWeatherNight,
mdiPackage,
mdiPackageDown,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { updateIsInstalling, UpdateEntity } from "../../data/update";
@@ -109,9 +108,11 @@ export const domainIcon = (
case "outlet":
return compareState === "on" ? mdiPowerPlug : mdiPowerPlugOff;
case "switch":
return compareState === "on" ? mdiToggleSwitch : mdiToggleSwitchOff;
return compareState === "on"
? mdiToggleSwitchVariant
: mdiToggleSwitchVariantOff;
default:
return mdiLightSwitch;
return mdiToggleSwitchVariant;
}
case "sensor": {

View File

@@ -347,8 +347,8 @@ class StatisticsChart extends LitElement {
statTypes.forEach((type) => {
let val: number | null;
if (type === "sum") {
if (!initVal) {
initVal = val = stat.state;
if (initVal === null) {
initVal = val = stat.state || 0;
prevSum = stat.sum;
} else {
val = initVal + ((stat.sum || 0) - prevSum!);

View File

@@ -52,6 +52,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public value?: string;
@property() public helper?: string;
@property() public devices?: DeviceRegistryEntry[];
@property() public areas?: AreaRegistryEntry[];
@@ -269,6 +271,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
? this.hass.localize("ui.components.device-picker.device")
: this.label}
.value=${this._value}
.helper=${this.helper}
.renderer=${rowRenderer}
.disabled=${this.disabled}
.required=${this.required}

View File

@@ -4,6 +4,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "./ha-device-picker";
import type { HaDevicePickerDeviceFilterFunc } from "./ha-device-picker";
@customElement("ha-devices-picker")
class HaDevicesPicker extends LitElement {
@@ -11,6 +12,10 @@ class HaDevicesPicker extends LitElement {
@property() public value?: string[];
@property() public helper?: string;
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
/**
@@ -37,6 +42,8 @@ class HaDevicesPicker extends LitElement {
@property({ attribute: "pick-device-label" }) public pickDeviceLabel?: string;
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
@@ -51,11 +58,13 @@ class HaDevicesPicker extends LitElement {
allow-custom-entity
.curValue=${entityId}
.hass=${this.hass}
.deviceFilter=${this.deviceFilter}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.value=${entityId}
.label=${this.pickedDeviceLabel}
.disabled=${this.disabled}
@value-changed=${this._deviceChanged}
></ha-device-picker>
</div>
@@ -63,12 +72,16 @@ class HaDevicesPicker extends LitElement {
)}
<div>
<ha-device-picker
allow-custom-entity
.hass=${this.hass}
.helper=${this.helper}
.deviceFilter=${this.deviceFilter}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.label=${this.pickDeviceLabel}
.required=${this.required}
.disabled=${this.disabled}
.required=${this.required && !currentDevices.length}
@value-changed=${this._addDevice}
></ha-device-picker>
</div>

View File

@@ -14,8 +14,12 @@ class HaEntitiesPickerLight extends LitElement {
@property({ type: Array }) public value?: string[];
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
@property() public helper?: string;
/**
* Show entities from specific domains.
* @type {string}
@@ -94,6 +98,7 @@ class HaEntitiesPickerLight extends LitElement {
.entityFilter=${this._entityFilter}
.value=${entityId}
.label=${this.pickedEntityLabel}
.disabled=${this.disabled}
@value-changed=${this._entityChanged}
></ha-entity-picker>
</div>
@@ -101,6 +106,7 @@ class HaEntitiesPickerLight extends LitElement {
)}
<div>
<ha-entity-picker
allow-custom-entity
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
@@ -110,7 +116,9 @@ class HaEntitiesPickerLight extends LitElement {
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter}
.label=${this.pickEntityLabel}
.required=${this.required}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required && !currentEntities.length}
@value-changed=${this._addEntity}
></ha-entity-picker>
</div>

View File

@@ -28,6 +28,8 @@ class HaEntityAttributePicker extends LitElement {
@property() public value?: string;
@property() public helper?: string;
@property({ type: Boolean }) private _opened = false;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
@@ -64,6 +66,7 @@ class HaEntityAttributePicker extends LitElement {
)}
.disabled=${this.disabled || !this.entityId}
.required=${this.required}
.helper=${this.helper}
.allowCustomValue=${this.allowCustomValue}
item-value-path="value"
item-label-path="label"

View File

@@ -48,6 +48,8 @@ export class HaEntityPicker extends LitElement {
@property() public value?: string;
@property() public helper?: string;
/**
* Show entities from specific domains.
* @type {Array}
@@ -304,6 +306,7 @@ export class HaEntityPicker extends LitElement {
.label=${this.label === undefined
? this.hass.localize("ui.components.entity.entity-picker.entity")
: this.label}
.helper=${this.helper}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._states}
.renderer=${rowRenderer}

View File

@@ -29,6 +29,8 @@ class HaAddonPicker extends LitElement {
@property() public value = "";
@property() public helper?: string;
@state() private _addons?: HassioAddonInfo[];
@property({ type: Boolean }) public disabled = false;
@@ -62,6 +64,7 @@ class HaAddonPicker extends LitElement {
.value=${this._value}
.required=${this.required}
.disabled=${this.disabled}
.helper=${this.helper}
.renderer=${rowRenderer}
.items=${this._addons}
item-value-path="slug"

View File

@@ -2,12 +2,12 @@ import "@polymer/paper-tooltip/paper-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { Analytics, AnalyticsPreferences } from "../data/analytics";
import type { Analytics, AnalyticsPreferences } from "../data/analytics";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import "./ha-checkbox";
import type { HaCheckbox } from "./ha-checkbox";
import type { HomeAssistant } from "../types";
import "./ha-settings-row";
import "./ha-switch";
import type { HaSwitch } from "./ha-switch";
const ADDITIONAL_PREFERENCES = [
{
@@ -40,62 +40,62 @@ export class HaAnalytics extends LitElement {
return html`
<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${baseEnabled}
.preference=${"base"}
.disabled=${loading}
name="base"
>
</ha-checkbox>
</span>
<span slot="heading" data-for="base"> Basic analytics </span>
<span slot="description" data-for="base">
This includes information about your system.
</span>
<ha-switch
@change=${this._handleRowClick}
.checked=${baseEnabled}
.preference=${"base"}
.disabled=${loading}
name="base"
>
</ha-switch>
</ha-settings-row>
${ADDITIONAL_PREFERENCES.map(
(preference) =>
html`<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${this.analytics?.preferences[preference.key]}
.preference=${preference.key}
name=${preference.key}
>
</ha-checkbox>
${!baseEnabled
? html`<paper-tooltip animation-delay="0" position="right">
You need to enable basic analytics for this option to be
available
</paper-tooltip>`
: ""}
</span>
<span slot="heading" data-for=${preference.key}>
${preference.title}
</span>
<span slot="description" data-for=${preference.key}>
${preference.description}
</span>
</ha-settings-row>`
html`
<ha-settings-row>
<span slot="heading" data-for=${preference.key}>
${preference.title}
</span>
<span slot="description" data-for=${preference.key}>
${preference.description}
</span>
<span>
<ha-switch
@change=${this._handleRowClick}
.checked=${this.analytics?.preferences[preference.key]}
.preference=${preference.key}
name=${preference.key}
>
</ha-switch>
${!baseEnabled
? html`
<paper-tooltip animation-delay="0" position="right">
You need to enable basic analytics for this option to be
available
</paper-tooltip>
`
: ""}
</span>
</ha-settings-row>
`
)}
<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${this.analytics?.preferences.diagnostics}
.preference=${"diagnostics"}
.disabled=${loading}
name="diagnostics"
>
</ha-checkbox>
</span>
<span slot="heading" data-for="diagnostics"> Diagnostics </span>
<span slot="description" data-for="diagnostics">
Share crash reports when unexpected errors occur.
</span>
<ha-switch
@change=${this._handleRowClick}
.checked=${this.analytics?.preferences.diagnostics}
.preference=${"diagnostics"}
.disabled=${loading}
name="diagnostics"
>
</ha-switch>
</ha-settings-row>
`;
}
@@ -120,23 +120,23 @@ export class HaAnalytics extends LitElement {
});
}
private _handleRowCheckboxClick(ev: Event) {
const checkbox = ev.currentTarget as HaCheckbox;
const preference = (checkbox as any).preference;
private _handleRowClick(ev: Event) {
const target = ev.currentTarget as HaSwitch;
const preference = (target as any).preference;
const preferences = this.analytics ? { ...this.analytics.preferences } : {};
if (preferences[preference] === checkbox.checked) {
if (preferences[preference] === target.checked) {
return;
}
preferences[preference] = checkbox.checked;
preferences[preference] = target.checked;
if (
ADDITIONAL_PREFERENCES.some((entry) => entry.key === preference) &&
checkbox.checked
target.checked
) {
preferences.base = true;
} else if (preference === "base" && !checkbox.checked) {
} else if (preference === "base" && !target.checked) {
preferences.usage = false;
preferences.statistics = false;
}

View File

@@ -49,6 +49,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@property() public value?: string;
@property() public helper?: string;
@property() public placeholder?: string;
@property({ type: Boolean, attribute: "no-add" })
@@ -312,6 +314,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
return html`
<ha-combo-box
.hass=${this.hass}
.helper=${this.helper}
item-value-path="area_id"
item-id-path="area_id"
item-label-path="name"

View File

@@ -15,6 +15,8 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) {
@property() public value?: string[];
@property() public helper?: string;
@property() public placeholder?: string;
@property({ type: Boolean, attribute: "no-add" })
@@ -90,6 +92,7 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) {
.noAdd=${this.noAdd}
.hass=${this.hass}
.label=${this.pickAreaLabel}
.helper=${this.helper}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
@@ -97,7 +100,7 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) {
.entityFilter=${this.entityFilter}
.disabled=${this.disabled}
.placeholder=${this.placeholder}
.required=${this.required}
.required=${this.required && !currentAreas.length}
@value-changed=${this._addArea}
></ha-area-picker>
</div>

View File

@@ -5,6 +5,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import "./ha-select";
import "./ha-textfield";
import "./ha-input-helper-text";
export interface TimeChangedEvent {
days?: number;
@@ -130,7 +131,7 @@ export class HaBaseTimeInput extends LitElement {
protected render(): TemplateResult {
return html`
${this.label
? html`<label>${this.label}${this.required ? "*" : ""}</label>`
? html`<label>${this.label}${this.required ? " *" : ""}</label>`
: ""}
<div class="time-input-wrap">
${this.enableDay
@@ -253,7 +254,9 @@ export class HaBaseTimeInput extends LitElement {
<mwc-list-item value="PM">PM</mwc-list-item>
</ha-select>`}
</div>
${this.helper ? html`<div class="helper">${this.helper}</div>` : ""}
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
`;
}
@@ -350,13 +353,6 @@ export class HaBaseTimeInput extends LitElement {
color: var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87));
padding-left: 4px;
}
.helper {
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
font-size: 0.75rem;
padding-left: 16px;
padding-right: 16px;
}
`;
}

View File

@@ -117,6 +117,19 @@ export class HaButtonToggleGroup extends LitElement {
--mdc-shape-small: 4px;
border-right-width: 1px;
}
:host([dir="rtl"]) ha-icon-button:first-child,
:host([dir="rtl"]) mwc-button:first-child {
border-radius: 0 4px 4px 0;
border-right-width: 1px;
--mdc-shape-small: 0 4px 4px 0;
--mdc-button-outline-width: 1px;
}
:host([dir="rtl"]) ha-icon-button:last-child,
:host([dir="rtl"]) mwc-button:last-child {
--mdc-shape-small: 4px 0 0 4px;
border-radius: 4px 0 0 4px;
}
`;
}
}

View File

@@ -0,0 +1,68 @@
import { ListItemBase } from "@material/mwc-list/mwc-list-item-base";
import { styles } from "@material/mwc-list/mwc-list-item.css";
import { css, CSSResult, html } from "lit";
import { customElement, property, query } from "lit/decorators";
@customElement("ha-clickable-list-item")
export class HaClickableListItem extends ListItemBase {
@property() public href?: string;
@property({ type: Boolean }) public disableHref = false;
// property used only in css
@property({ type: Boolean, reflect: true }) public rtl = false;
@query("a") private _anchor!: HTMLAnchorElement;
public render() {
const r = super.render();
const href = this.href || "";
return html`${this.disableHref
? html`<a aria-role="option">${r}</a>`
: html`<a aria-role="option" href=${href}>${r}</a>`}`;
}
firstUpdated() {
super.firstUpdated();
this.addEventListener("keydown", (ev) => {
if (ev.key === "Enter" || ev.key === " ") {
this._anchor.click();
}
});
}
static get styles(): CSSResult[] {
return [
styles,
css`
:host {
padding-left: 0px;
padding-right: 0px;
}
:host([rtl]) span {
margin-left: var(--mdc-list-item-graphic-margin, 20px) !important;
margin-right: 0px !important;
}
:host([graphic="avatar"]:not([twoLine])),
:host([graphic="icon"]:not([twoLine])) {
height: 48px;
}
a {
width: 100%;
height: 100%;
display: flex;
align-items: center;
padding-left: var(--mdc-list-side-padding, 20px);
padding-right: var(--mdc-list-side-padding, 20px);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-clickable-list-item": HaClickableListItem;
}
}

View File

@@ -64,6 +64,8 @@ export class HaComboBox extends LitElement {
@property() public validationMessage?: string;
@property() public helper?: string;
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean }) public invalid?: boolean;
@@ -147,6 +149,8 @@ export class HaComboBox extends LitElement {
.suffix=${html`<div style="width: 28px;"></div>`}
.icon=${this.icon}
.invalid=${this.invalid}
.helper=${this.helper}
helperPersistent
>
<slot name="icon" slot="leadingIcon"></slot>
</ha-textfield>

View File

@@ -39,11 +39,15 @@ export class HaDateInput extends LitElement {
@property() public label?: string;
@property() public helper?: string;
render() {
return html`<ha-textfield
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
iconTrailing
helperPersistent
@click=${this._openDialog}
.value=${this.value
? formatDateNumeric(new Date(this.value), this.locale)

View File

@@ -77,7 +77,7 @@ export class HaForm extends LitElement implements HaFormElement {
protected render(): TemplateResult {
return html`
<div class="root">
<div class="root" part="root">
${this.error && this.error.base
? html`
<ha-alert alert-type="error">
@@ -173,7 +173,6 @@ export class HaForm extends LitElement implements HaFormElement {
}
static get styles(): CSSResultGroup {
// .root has overflow: auto to avoid margin collapse
return css`
.root {
margin-bottom: -24px;

View File

@@ -31,6 +31,8 @@ export class HaIconPicker extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property() public placeholder?: string;
@property() public fallbackPath?: string;
@@ -57,6 +59,7 @@ export class HaIconPicker extends LitElement {
allow-custom-value
.filteredItems=${iconItems}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.placeholder=${this.placeholder}

View File

@@ -0,0 +1,25 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-input-helper-text")
class InputHelperText extends LitElement {
protected render(): TemplateResult {
return html`<slot></slot>`;
}
static styles = css`
:host {
display: block;
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
font-size: 0.75rem;
padding-left: 16px;
padding-right: 16px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-input-helper-text": InputHelperText;
}
}

View File

@@ -46,11 +46,14 @@ class HaLabeledSlider extends PolymerElement {
value="{{value}}"
></ha-slider>
</div>
<template is="dom-if" if="[[helper]]">
<ha-input-helper-text>[[helper]]</ha-input-helper-text>
</template>
`;
}
_getTitle() {
return `${this.caption}${this.required ? "*" : ""}`;
return `${this.caption}${this.caption && this.required ? " *" : ""}`;
}
static get properties() {
@@ -62,6 +65,7 @@ class HaLabeledSlider extends PolymerElement {
max: Number,
pin: Boolean,
step: Number,
helper: String,
extra: {
type: Boolean,

View File

@@ -3,7 +3,6 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { subscribeNotifications } from "../data/persistent_notification";
import { HomeAssistant } from "../types";
import "./ha-icon-button";
@@ -43,18 +42,15 @@ class HaMenuButton extends LitElement {
protected render(): TemplateResult {
const hasNotifications =
(this.narrow || this.hass.dockedSidebar === "always_hidden") &&
(this._hasNotifications ||
Object.keys(this.hass.states).some(
(entityId) => computeDomain(entityId) === "configurator"
));
this._hasNotifications &&
(this.narrow || this.hass.dockedSidebar === "always_hidden");
return html`
<ha-icon-button
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
.path=${mdiMenu}
@click=${this._toggleMenu}
></ha-icon-button>
${hasNotifications ? html` <div class="dot"></div> ` : ""}
${hasNotifications ? html`<div class="dot"></div>` : ""}
`;
}

View File

@@ -0,0 +1,92 @@
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import type { HomeAssistant } from "../types";
import "./ha-icon-next";
import "./ha-svg-icon";
import "./ha-clickable-list-item";
@customElement("ha-navigation-list")
class HaNavigationList extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public pages!: PageNavigation[];
@property({ type: Boolean }) public hasSecondary = false;
public render(): TemplateResult {
return html`
<mwc-list>
${this.pages.map(
(page) => html`
<ha-clickable-list-item
graphic="avatar"
.twoline=${this.hasSecondary}
.hasMeta=${!this.narrow}
@click=${this._entryClicked}
href=${page.path}
>
<div
slot="graphic"
class=${page.iconColor ? "icon-background" : ""}
.style="background-color: ${page.iconColor || "undefined"}"
>
<ha-svg-icon .path=${page.iconPath}></ha-svg-icon>
</div>
<span>${page.name}</span>
${this.hasSecondary
? html`<span slot="secondary">${page.description}</span>`
: ""}
${!this.narrow
? html`<ha-icon-next slot="meta"></ha-icon-next>`
: ""}
</ha-clickable-list-item>
`
)}
</mwc-list>
`;
}
private _entryClicked(ev) {
ev.currentTarget.blur();
}
static styles: CSSResultGroup = css`
a {
text-decoration: none;
color: var(--primary-text-color);
position: relative;
display: block;
outline: 0;
}
ha-svg-icon,
ha-icon-next {
color: var(--secondary-text-color);
height: 24px;
width: 24px;
}
ha-svg-icon {
padding: 8px;
}
.icon-background {
border-radius: 50%;
}
.icon-background ha-svg-icon {
color: #fff;
}
mwc-list-item {
cursor: pointer;
font-size: var(--navigation-list-item-title-font-size);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-navigation-list": HaNavigationList;
}
}

View File

@@ -14,6 +14,8 @@ export class HaAddonSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -23,6 +25,7 @@ export class HaAddonSelector extends LitElement {
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-entity

View File

@@ -18,6 +18,8 @@ export class HaAreaSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@state() public _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
@@ -47,6 +49,7 @@ export class HaAreaSelector extends LitElement {
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
no-add
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
@@ -66,6 +69,7 @@ export class HaAreaSelector extends LitElement {
<ha-areas-picker
.hass=${this.hass}
.value=${this.value}
.helper=${this.helper}
.pickAreaLabel=${this.label}
no-add
.deviceFilter=${this._filterDevices}

View File

@@ -16,6 +16,8 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -32,6 +34,7 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
this.context?.filter_entity}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-value

View File

@@ -4,6 +4,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import "../ha-formfield";
import "../ha-switch";
import "../ha-input-helper-text";
@customElement("ha-selector-boolean")
export class HaBooleanSelector extends LitElement {
@@ -13,16 +14,23 @@ export class HaBooleanSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`<ha-formfield alignEnd spaceBetween .label=${this.label}>
<ha-switch
.checked=${this.value}
@change=${this._handleChange}
.disabled=${this.disabled}
></ha-switch>
</ha-formfield>`;
return html`
<ha-formfield alignEnd spaceBetween .label=${this.label}>
<ha-switch
.checked=${this.value}
@change=${this._handleChange}
.disabled=${this.disabled}
></ha-switch>
</ha-formfield>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
`;
}
private _handleChange(ev) {
@@ -35,12 +43,10 @@ export class HaBooleanSelector extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
height: 56px;
display: flex;
}
ha-formfield {
width: 100%;
display: flex;
height: 56px;
align-items: center;
--mdc-typography-body2-font-size: 1em;
}
`;

View File

@@ -16,6 +16,8 @@ export class HaColorRGBSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -24,9 +26,11 @@ export class HaColorRGBSelector extends LitElement {
return html`
<ha-textfield
type="color"
helperPersistent
.value=${this.value ? rgb2hex(this.value as any) : ""}
.label=${this.label || ""}
.required=${this.required}
.helper=${this.helper}
.disalbled=${this.disabled}
@change=${this._valueChanged}
></ha-textfield>

View File

@@ -15,6 +15,8 @@ export class HaColorTempSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -24,11 +26,12 @@ export class HaColorTempSelector extends LitElement {
<ha-labeled-slider
pin
icon="hass:thermometer"
.caption=${this.label}
.caption=${this.label || ""}
.min=${this.selector.color_temp.min_mireds ?? 153}
.max=${this.selector.color_temp.max_mireds ?? 500}
.value=${this.value}
.disabled=${this.disabled}
.helper=${this.helper}
.required=${this.required}
@change=${this._valueChanged}
></ha-labeled-slider>

View File

@@ -14,6 +14,8 @@ export class HaDateSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -26,6 +28,7 @@ export class HaDateSelector extends LitElement {
.disabled=${this.disabled}
.value=${this.value}
.required=${this.required}
.helper=${this.helper}
>
</ha-date-input>
`;

View File

@@ -6,6 +6,7 @@ import type { HomeAssistant } from "../../types";
import "../ha-date-input";
import type { HaDateInput } from "../ha-date-input";
import "../ha-time-input";
import "../ha-input-helper-text";
import type { HaTimeInput } from "../ha-time-input";
@customElement("ha-selector-datetime")
@@ -18,6 +19,8 @@ export class HaDateTimeSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -30,23 +33,28 @@ export class HaDateTimeSelector extends LitElement {
const values = this.value?.split(" ");
return html`
<ha-date-input
.label=${this.label}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
.value=${values?.[0]}
@value-changed=${this._valueChanged}
>
</ha-date-input>
<ha-time-input
enable-second
.value=${values?.[1] || "0:00:00"}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
@value-changed=${this._valueChanged}
></ha-time-input>
<div class="input">
<ha-date-input
.label=${this.label}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
.value=${values?.[0]}
@value-changed=${this._valueChanged}
>
</ha-date-input>
<ha-time-input
enable-second
.value=${values?.[1] || "0:00:00"}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
@value-changed=${this._valueChanged}
></ha-time-input>
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
`;
}
@@ -58,7 +66,7 @@ export class HaDateTimeSelector extends LitElement {
}
static styles = css`
:host {
.input {
display: flex;
align-items: center;
flex-direction: row;

View File

@@ -17,6 +17,8 @@ export class HaDeviceSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@state() public _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
@@ -43,6 +45,7 @@ export class HaDeviceSelector extends LitElement {
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
@@ -62,12 +65,15 @@ export class HaDeviceSelector extends LitElement {
<ha-devices-picker
.hass=${this.hass}
.value=${this.value}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.disabled=${this.disabled}
.required=${this.required}
></ha-devices-picker>
`;

View File

@@ -23,6 +23,8 @@ export class HaEntitySelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -33,6 +35,7 @@ export class HaEntitySelector extends LitElement {
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities}
.entityFilter=${this._filterEntities}
@@ -47,9 +50,11 @@ export class HaEntitySelector extends LitElement {
<ha-entities-picker
.hass=${this.hass}
.value=${this.value}
.entityFilter=${this._filterEntities}
.helper=${this.helper}
.includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities}
.entityFilter=${this._filterEntities}
.disabled=${this.disabled}
.required=${this.required}
></ha-entities-picker>
`;

View File

@@ -15,6 +15,8 @@ export class HaIconSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -26,6 +28,7 @@ export class HaIconSelector extends LitElement {
.value=${this.value}
.required=${this.required}
.disabled=${this.disabled}
.helper=${this.helper}
.fallbackPath=${this.selector.icon.fallbackPath}
.placeholder=${this.selector.icon.placeholder}
@value-changed=${this._valueChanged}

View File

@@ -20,6 +20,8 @@ export class HaLocationSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
@@ -27,6 +29,7 @@ export class HaLocationSelector extends LitElement {
<ha-locations-editor
class="flex"
.hass=${this.hass}
.helper=${this.helper}
.locations=${this._location(this.selector, this.value)}
@location-updated=${this._locationChanged}
@radius-updated=${this._radiusChanged}

View File

@@ -33,6 +33,8 @@ export class HaMediaSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean, reflect: true }) public required = true;
@@ -86,6 +88,7 @@ export class HaMediaSelector extends LitElement {
.label=${this.label ||
this.hass.localize("ui.components.selectors.media.pick_media_player")}
.disabled=${this.disabled}
.helper=${this.helper}
.required=${this.required}
include-domains='["media_player"]'
allow-custom-entity

View File

@@ -6,6 +6,7 @@ import { NumberSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-slider";
import "../ha-textfield";
import "../ha-input-helper-text";
@customElement("ha-selector-number")
export class HaNumberSelector extends LitElement {
@@ -26,8 +27,13 @@ export class HaNumberSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`${this.selector.number.mode !== "box"
? html`${this.label}${this.required ? "*" : ""}<ha-slider
const isBox = this.selector.number.mode === "box";
return html`
${this.label ? html`${this.label}${this.required ? " *" : ""}` : ""}
<div class="input">
${!isBox
? html`<ha-slider
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
@@ -39,28 +45,33 @@ export class HaNumberSelector extends LitElement {
@change=${this._handleSliderChange}
>
</ha-slider>`
: ""}
<ha-textfield
inputMode="numeric"
pattern="[0-9]+([\\.][0-9]+)?"
.label=${this.selector.number.mode !== "box" ? undefined : this.label}
.placeholder=${this.placeholder}
class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this.value ?? ""}
.step=${this.selector.number.step ?? 1}
helperPersistent
.helper=${isBox ? this.helper : undefined}
.disabled=${this.disabled}
.required=${this.required}
.suffix=${this.selector.number.unit_of_measurement}
type="number"
autoValidate
?no-spinner=${this.selector.number.mode !== "box"}
@input=${this._handleInputChange}
>
</ha-textfield>
</div>
${!isBox && this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
<ha-textfield
inputMode="numeric"
pattern="[0-9]+([\\.][0-9]+)?"
.label=${this.selector.number.mode !== "box" ? undefined : this.label}
.placeholder=${this.placeholder}
class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this.value ?? ""}
.step=${this.selector.number.step ?? 1}
helperPersistent
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.suffix=${this.selector.number.unit_of_measurement}
type="number"
autoValidate
?no-spinner=${this.selector.number.mode !== "box"}
@input=${this._handleInputChange}
>
</ha-textfield>`;
`;
}
private get _value() {
@@ -92,7 +103,7 @@ export class HaNumberSelector extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
.input {
display: flex;
justify-content: space-between;
align-items: center;

View File

@@ -3,6 +3,7 @@ import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import "../ha-yaml-editor";
import "../ha-input-helper-text";
@customElement("ha-selector-object")
export class HaObjectSelector extends LitElement {
@@ -12,6 +13,8 @@ export class HaObjectSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property() public placeholder?: string;
@property({ type: Boolean }) public disabled = false;
@@ -20,13 +23,17 @@ export class HaObjectSelector extends LitElement {
protected render() {
return html`<ha-yaml-editor
.hass=${this.hass}
.readonly=${this.disabled}
.required=${this.required}
.placeholder=${this.placeholder}
.defaultValue=${this.value}
@value-changed=${this._handleChange}
></ha-yaml-editor>`;
.hass=${this.hass}
.readonly=${this.disabled}
.label=${this.label}
.required=${this.required}
.placeholder=${this.placeholder}
.defaultValue=${this.value}
@value-changed=${this._handleChange}
></ha-yaml-editor>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""} `;
}
private _handleChange(ev) {

View File

@@ -58,6 +58,7 @@ export class HaSelectSelector extends LitElement {
`
)}
</div>
${this._renderHelper()}
`;
}
@@ -76,6 +77,7 @@ export class HaSelectSelector extends LitElement {
`
)}
</div>
${this._renderHelper()}
`;
}
@@ -107,8 +109,9 @@ export class HaSelectSelector extends LitElement {
item-label-path="label"
.hass=${this.hass}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.required=${this.required && !value.length}
.value=${this._filter}
.items=${options.filter((item) => !this.value?.includes(item.value))}
@filter-changed=${this._filterChanged}
@@ -131,6 +134,7 @@ export class HaSelectSelector extends LitElement {
item-label-path="label"
.hass=${this.hass}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.items=${options}
@@ -161,6 +165,12 @@ export class HaSelectSelector extends LitElement {
`;
}
private _renderHelper() {
return this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: "";
}
private get _mode(): "list" | "dropdown" {
return (
this.selector.select.mode ||

View File

@@ -26,6 +26,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
@property() public label?: string;
@property() public helper?: string;
@state() private _entityPlaformLookup?: Record<string, string>;
@state() private _configEntries?: ConfigEntry[];
@@ -64,6 +66,7 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
return html`<ha-target-picker
.hass=${this.hass}
.value=${this.value}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.entityRegFilter=${this._filterRegEntities}
.entityFilter=${this._filterEntities}

View File

@@ -0,0 +1,56 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import "../ha-code-editor";
import "../ha-input-helper-text";
@customElement("ha-selector-template")
export class HaTemplateSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public value?: string;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
${this.label
? html`<p>${this.label}${this.required ? " *" : ""}</p>`
: ""}
<ha-code-editor
mode="jinja2"
.hass=${this.hass}
.value=${this.value}
.readOnly=${this.disabled}
autofocus
autocomplete-entities
@value-changed=${this._handleChange}
dir="ltr"
></ha-code-editor>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
`;
}
private _handleChange(ev) {
const value = ev.target.value;
if (this.value === value) {
return;
}
fireEvent(this, "value-changed", { value });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-template": HaTemplateSelector;
}
}

View File

@@ -14,6 +14,8 @@ export class HaTimeSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@@ -25,6 +27,7 @@ export class HaTimeSelector extends LitElement {
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
.helper=${this.helper}
.label=${this.label}
enable-second
></ha-time-input>

View File

@@ -18,6 +18,7 @@ import "./ha-selector-number";
import "./ha-selector-object";
import "./ha-selector-select";
import "./ha-selector-target";
import "./ha-selector-template";
import "./ha-selector-text";
import "./ha-selector-time";
import "./ha-selector-icon";

View File

@@ -36,10 +36,9 @@ import memoizeOne from "memoize-one";
import { LocalStorage } from "../common/decorators/local-storage";
import { fireEvent } from "../common/dom/fire_event";
import { toggleAttribute } from "../common/dom/toggle_attribute";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { stringCompare } from "../common/string/compare";
import { computeRTL } from "../common/util/compute_rtl";
import { throttle } from "../common/util/throttle";
import { ActionHandlerDetail } from "../data/lovelace";
import {
PersistentNotification,
@@ -294,11 +293,7 @@ class HaSidebar extends LitElement {
toggleAttribute(this, "rtl", computeRTL(this.hass));
}
this._updatesCount = Object.values(this.hass.states).filter(
(entity) =>
computeStateDomain(entity) === "update" &&
updateCanInstall(entity as UpdateEntity)
).length;
this._calculateCounts();
if (!SUPPORT_SCROLL_IF_NEEDED) {
return;
@@ -312,6 +307,21 @@ class HaSidebar extends LitElement {
}
}
private _calculateCounts = throttle(() => {
let updateCount = 0;
for (const entityId of Object.keys(this.hass.states)) {
if (
entityId.startsWith("update.") &&
updateCanInstall(this.hass.states[entityId] as UpdateEntity)
) {
updateCount++;
}
}
this._updatesCount = updateCount;
}, 5000);
private _renderHeader() {
return html`<div
class="menu"
@@ -519,14 +529,9 @@ class HaSidebar extends LitElement {
}
private _renderNotifications() {
let notificationCount = this._notifications
const notificationCount = this._notifications
? this._notifications.length
: 0;
for (const entityId in this.hass.states) {
if (computeDomain(entityId) === "configurator") {
notificationCount++;
}
}
return html`<div
class="notifications-container"
@@ -1034,6 +1039,8 @@ class HaSidebar extends LitElement {
.notification-badge,
.configuration-badge {
left: calc(var(--app-drawer-width) - 42px);
position: absolute;
min-width: 20px;
box-sizing: border-box;
border-radius: 50%;

View File

@@ -43,6 +43,7 @@ import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
import "./ha-area-picker";
import "./ha-icon-button";
import "./ha-svg-icon";
import "./ha-input-helper-text";
@customElement("ha-target-picker")
export class HaTargetPicker extends SubscribeMixin(LitElement) {
@@ -52,6 +53,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property() public label?: string;
@property() public helper?: string;
/**
* Show only targets with entities from specific domains.
* @type {Array}
@@ -213,7 +216,11 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
</span>
</span>
</div>
</div>`;
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""} `;
}
private async _showPicker(ev) {

View File

@@ -14,6 +14,8 @@ export class HaTimeInput extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@@ -46,6 +48,7 @@ export class HaTimeInput extends LitElement {
@value-changed=${this._timeChanged}
.enableSecond=${this.enableSecond}
.required=${this.required}
.helper=${this.helper}
></ha-base-time-input>
`;
}

View File

@@ -61,7 +61,9 @@ export class HaYamlEditor extends LitElement {
return html``;
}
return html`
${this.label ? html`<p>${this.label}${this.required ? "*" : ""}</p>` : ""}
${this.label
? html`<p>${this.label}${this.required ? " *" : ""}</p>`
: ""}
<ha-code-editor
.hass=${this.hass}
.value=${this._yaml}

View File

@@ -19,6 +19,7 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
import type { HomeAssistant } from "../../types";
import "../ha-input-helper-text";
import "./ha-map";
import type { HaMap } from "./ha-map";
@@ -50,6 +51,8 @@ export class HaLocationsEditor extends LitElement {
@property({ attribute: false }) public locations?: MarkerLocation[];
@property() public helper?: string;
@property({ type: Boolean }) public autoFit = false;
@property({ type: Number }) public zoom = 16;
@@ -102,13 +105,18 @@ export class HaLocationsEditor extends LitElement {
}
protected render(): TemplateResult {
return html`<ha-map
.hass=${this.hass}
.layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom}
.autoFit=${this.autoFit}
.darkMode=${this.darkMode}
></ha-map>`;
return html`
<ha-map
.hass=${this.hass}
.layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom}
.autoFit=${this.autoFit}
.darkMode=${this.darkMode}
></ha-map>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
`;
}
private _getLayers = memoizeOne(
@@ -287,11 +295,8 @@ export class HaLocationsEditor extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
height: 300px;
}
ha-map {
display: block;
height: 100%;
}
`;

View File

@@ -8,6 +8,8 @@ import { BlueprintInput } from "./blueprint";
import { DeviceCondition, DeviceTrigger } from "./device_automation";
import { Action, MODES } from "./script";
export const AUTOMATION_DEFAULT_MODE: ManualAutomationConfig["mode"] = "single";
export interface AutomationEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & {
id?: string;
@@ -67,7 +69,7 @@ export interface BaseTrigger {
export interface StateTrigger extends BaseTrigger {
platform: "state";
entity_id: string;
entity_id: string | string[];
attribute?: string;
from?: string | number;
to?: string | string[] | number;

View File

@@ -21,7 +21,8 @@ export type AddonState = "started" | "stopped" | null;
export type AddonRepository = "core" | "local" | string;
interface AddonTranslations {
[key: string]: Record<string, Record<string, Record<string, string>>>;
network?: Record<string, string>;
configuration?: Record<string, { name?: string; description?: string }>;
}
export interface HassioAddonInfo {
@@ -91,7 +92,7 @@ export interface HassioAddonDetails extends HassioAddonInfo {
slug: string;
startup: AddonStartup;
stdin: boolean;
translations: AddonTranslations;
translations: Record<string, AddonTranslations>;
watchdog: null | boolean;
webui: null | string;
}

View File

@@ -194,11 +194,24 @@ export interface ChooseAction {
default?: Action | Action[];
}
export interface IfAction {
alias?: string;
if: string | Condition[];
then: Action | Action[];
else?: Action | Action[];
}
export interface VariablesAction {
alias?: string;
variables: Record<string, unknown>;
}
export interface StopAction {
alias?: string;
stop: string;
error?: boolean;
}
interface UnknownAction {
alias?: string;
[key: string]: unknown;
@@ -215,8 +228,10 @@ export type Action =
| WaitForTriggerAction
| RepeatAction
| ChooseAction
| IfAction
| VariablesAction
| PlayMediaAction
| StopAction
| UnknownAction;
export interface ActionTypes {
@@ -228,10 +243,12 @@ export interface ActionTypes {
activate_scene: SceneAction;
repeat: RepeatAction;
choose: ChooseAction;
if: IfAction;
wait_for_trigger: WaitForTriggerAction;
variables: VariablesAction;
service: ServiceAction;
play_media: PlayMediaAction;
stop: StopAction;
unknown: UnknownAction;
}
@@ -299,12 +316,18 @@ export const getActionType = (action: Action): ActionType => {
if ("choose" in action) {
return "choose";
}
if ("if" in action) {
return "if";
}
if ("wait_for_trigger" in action) {
return "wait_for_trigger";
}
if ("variables" in action) {
return "variables";
}
if ("stop" in action) {
return "stop";
}
if ("service" in action) {
if ("metadata" in action) {
if (is(action, activateSceneActionStruct)) {

View File

@@ -19,6 +19,7 @@ export type Selector =
| SelectSelector
| StringSelector
| TargetSelector
| TemplateSelector
| ThemeSelector
| TimeSelector;
@@ -213,6 +214,11 @@ export interface TargetSelector {
};
}
export interface TemplateSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
template: {};
}
export interface ThemeSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
theme: {};

View File

@@ -13,7 +13,8 @@ export const UPDATE_SUPPORT_BACKUP = 8;
export const UPDATE_SUPPORT_RELEASE_NOTES = 16;
interface UpdateEntityAttributes extends HassEntityAttributeBase {
current_version: string | null;
auto_update: boolean | null;
installed_version: string | null;
in_progress: boolean | number;
latest_version: string | null;
release_summary: string | null;

View File

@@ -20,6 +20,7 @@ export const subscribeRenderTemplate = (
entity_ids?: string | string[];
variables?: Record<string, unknown>;
timeout?: number;
strict?: boolean;
}
): Promise<UnsubscribeFunc> =>
conn.subscribeMessage((msg: RenderTemplateResult) => onChange(msg), {

View File

@@ -29,6 +29,7 @@ import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import { fetchIntegrationManifest } from "../../data/integration";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
@@ -43,10 +44,10 @@ import "./step-flow-create-entry";
import "./step-flow-external";
import "./step-flow-form";
import "./step-flow-loading";
import "./step-flow-menu";
import "./step-flow-pick-flow";
import "./step-flow-pick-handler";
import "./step-flow-progress";
import "./step-flow-menu";
let instance = 0;
@@ -237,22 +238,32 @@ class DataEntryFlowDialog extends LitElement {
""
: html`
<div class="dialog-actions">
${["form", "menu", "external"].includes(
this._step?.type as any
)
${([
"form",
"menu",
"external",
"progress",
"data_entry_flow_progressed",
].includes(this._step?.type as any) &&
this._params.manifest?.is_built_in) ||
this._params.manifest?.documentation
? html`
<a
href=${documentationUrl(
this.hass,
`/integrations/${this._step!.handler}`
)}
href=${this._params.manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._params.manifest.domain}`
)
: this._params?.manifest?.documentation}
target="_blank"
rel="noreferrer noopener"
><ha-icon-button
>
<ha-icon-button
.label=${this.hass.localize("ui.common.help")}
.path=${mdiHelpCircle}
?rtl=${computeRTL(this.hass)}
></ha-icon-button
>
</ha-icon-button
></a>
`
: ""}
@@ -427,6 +438,17 @@ class DataEntryFlowDialog extends LitElement {
this._handler = undefined;
}
this._processStep(step);
if (this._params!.manifest === undefined) {
try {
this._params!.manifest = await fetchIntegrationManifest(
this.hass,
this._params?.domain || step.handler
);
} catch (_) {
// No manifest
this._params!.manifest = null;
}
}
} else {
this._step = null;
this._flowsInProgress = flowsInProgress;

View File

@@ -10,6 +10,7 @@ import {
DataEntryFlowStepMenu,
DataEntryFlowStepProgress,
} from "../../data/data_entry_flow";
import { IntegrationManifest } from "../../data/integration";
import { HomeAssistant } from "../../types";
export interface FlowHandlers {
@@ -122,6 +123,8 @@ export interface DataEntryFlowDialogParams {
startFlowHandler?: string;
searchQuery?: string;
continueFlowId?: string;
manifest?: IntegrationManifest | null;
domain?: string;
dialogClosedCallback?: (params: {
flowFinished: boolean;
entryId?: string;

View File

@@ -1,6 +1,6 @@
import { html } from "lit";
import { ConfigEntry } from "../../data/config_entries";
import { domainToName } from "../../data/integration";
import { domainToName, IntegrationManifest } from "../../data/integration";
import {
createOptionsFlow,
deleteOptionsFlow,
@@ -16,12 +16,15 @@ export const loadOptionsFlowDialog = loadDataEntryFlowDialog;
export const showOptionsFlowDialog = (
element: HTMLElement,
configEntry: ConfigEntry
configEntry: ConfigEntry,
manifest?: IntegrationManifest | null
): void =>
showFlowDialog(
element,
{
startFlowHandler: configEntry.entry_id,
domain: configEntry.domain,
manifest,
},
{
loadDevicesAndAreas: false,

View File

@@ -28,7 +28,7 @@ class StepFlowCreateEntry extends LitElement {
const localize = this.hass.localize;
return html`
<h2>Success!</h2>
<h2>${localize("ui.panel.config.integrations.config_flow.success")}!</h2>
<div class="content">
${this.flowConfig.renderCreateEntryDescription(this.hass, this.step)}
${this.step.result?.state === "not_loaded"
@@ -41,7 +41,11 @@ class StepFlowCreateEntry extends LitElement {
${this.devices.length === 0
? ""
: html`
<p>We found the following devices:</p>
<p>
${localize(
"ui.panel.config.integrations.config_flow.found_following_devices"
)}:
</p>
<div class="devices">
${this.devices.map(
(device) =>
@@ -49,7 +53,12 @@ class StepFlowCreateEntry extends LitElement {
<div class="device">
<div>
<b>${computeDeviceName(device, this.hass)}</b><br />
${device.model} (${device.manufacturer})
${!device.model && !device.manufacturer
? html`&nbsp;`
: html`${device.model}
${device.manufacturer
? html`(${device.manufacturer})`
: ""}`}
</div>
<ha-area-picker
.hass=${this.hass}

View File

@@ -190,6 +190,10 @@ class StepFlowForm extends LitElement {
margin-top: 24px;
display: block;
}
h2 {
word-break: break-word;
padding-right: 72px;
}
`,
];
}

View File

@@ -35,6 +35,7 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
.has-direction .container-direction,
.has-oscillating .container-oscillating {
display: block;
margin-top: 8px;
}
ha-select {

View File

@@ -21,6 +21,7 @@ import {
UPDATE_SUPPORT_SPECIFIC_VERSION,
} from "../../../data/update";
import type { HomeAssistant } from "../../../types";
import { BINARY_STATE_OFF } from "../../../common/const";
@customElement("more-info-update")
class MoreInfoUpdate extends LitElement {
@@ -56,21 +57,18 @@ class MoreInfoUpdate extends LitElement {
></mwc-linear-progress>`
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
: ""}
${this.stateObj.attributes.title
? html`<h3>${this.stateObj.attributes.title}</h3>`
: ""}
<h3>${this.stateObj.attributes.title}</h3>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="row">
<div class="key">
${this.hass.localize(
"ui.dialogs.more_info_control.update.current_version"
"ui.dialogs.more_info_control.update.installed_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.current_version ??
${this.stateObj.attributes.installed_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div>
@@ -130,19 +128,34 @@ class MoreInfoUpdate extends LitElement {
: ""}
<hr />
<div class="actions">
<mwc-button
@click=${this._handleSkip}
.disabled=${skippedVersion ||
this.stateObj.state === "off" ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize("ui.dialogs.more_info_control.update.skip")}
</mwc-button>
${this.stateObj.attributes.auto_update
? ""
: this.stateObj.state === BINARY_STATE_OFF &&
this.stateObj.attributes.skipped_version
? html`
<mwc-button @click=${this._handleClearSkipped}>
${this.hass.localize(
"ui.dialogs.more_info_control.update.clear_skipped"
)}
</mwc-button>
`
: html`
<mwc-button
@click=${this._handleSkip}
.disabled=${skippedVersion ||
this.stateObj.state === BINARY_STATE_OFF ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.skip"
)}
</mwc-button>
`}
${supportsFeature(this.stateObj, UPDATE_SUPPORT_INSTALL)
? html`
<mwc-button
@click=${this._handleInstall}
.disabled=${(this.stateObj.state === "off" &&
.disabled=${(this.stateObj.state === BINARY_STATE_OFF &&
!skippedVersion) ||
updateIsInstalling(this.stateObj)}
>
@@ -204,6 +217,12 @@ class MoreInfoUpdate extends LitElement {
});
}
private _handleClearSkipped(): void {
this.hass.callService("update", "clear_skipped", {
entity_id: this.stateObj!.entity_id,
});
}
static get styles(): CSSResultGroup {
return css`
hr {
@@ -237,6 +256,10 @@ class MoreInfoUpdate extends LitElement {
width: 100%;
justify-content: center;
}
mwc-linear-progress {
margin-bottom: -10px;
margin-top: -10px;
}
`;
}
}

View File

@@ -34,7 +34,10 @@ import { useAmPm } from "../../common/datetime/use_am_pm";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button-toggle-group";
import "../../components/ha-icon-button";
import "../../components/ha-icon-button-prev";
import "../../components/ha-icon-button-next";
import { haStyle } from "../../resources/styles";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import type {
CalendarEvent,
CalendarViewChanged,
@@ -124,26 +127,25 @@ export class HAFullCalendar extends LitElement {
"ui.components.calendar.today"
)}</mwc-button
>
<ha-icon-button
<ha-icon-button-prev
.label=${this.hass.localize("ui.common.previous")}
.path=${mdiChevronLeft}
class="prev"
@click=${this._handlePrev}
>
</ha-icon-button>
<ha-icon-button
</ha-icon-button-prev>
<ha-icon-button-next
.label=${this.hass.localize("ui.common.next")}
.path=${mdiChevronRight}
class="next"
@click=${this._handleNext}
>
</ha-icon-button>
</ha-icon-button-next>
</div>
<h1>${this.calendar.view.title}</h1>
<ha-button-toggle-group
.buttons=${viewToggleButtons}
.active=${this._activeView}
@value-changed=${this._handleView}
.dir=${computeRTLDirection(this.hass)}
></ha-button-toggle-group>
`
: html`
@@ -179,6 +181,7 @@ export class HAFullCalendar extends LitElement {
.buttons=${viewToggleButtons}
.active=${this._activeView}
@value-changed=${this._handleView}
.dir=${computeRTLDirection(this.hass)}
></ha-button-toggle-group>
</div>
`}

View File

@@ -202,7 +202,7 @@ class HaConfigAreaPage extends LitElement {
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.tabs=${configSections.devices}
.tabs=${configSections.areas}
.route=${this.route}
>
${this.narrow

View File

@@ -82,7 +82,7 @@ export class HaConfigAreasDashboard extends LitElement {
.narrow=${this.narrow}
.isWide=${this.isWide}
back-path="/config"
.tabs=${configSections.devices}
.tabs=${configSections.areas}
.route=${this.route}
>
<ha-icon-button

View File

@@ -32,9 +32,11 @@ import "./types/ha-automation-action-condition";
import "./types/ha-automation-action-delay";
import "./types/ha-automation-action-device_id";
import "./types/ha-automation-action-event";
import "./types/ha-automation-action-if";
import "./types/ha-automation-action-play_media";
import "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-service";
import "./types/ha-automation-action-stop";
import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template";
@@ -49,7 +51,9 @@ const OPTIONS = [
"wait_for_trigger",
"repeat",
"choose",
"if",
"device_id",
"stop",
];
const getType = (action: Action | undefined) => {

View File

@@ -165,6 +165,9 @@ export class HaChooseAction extends LitElement implements ActionElement {
right: 0;
padding: 4px;
}
ha-form::part(root) {
overflow: visible;
}
`,
];
}

View File

@@ -0,0 +1,109 @@
import { CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { Action, IfAction } from "../../../../../data/script";
import { HaDeviceCondition } from "../../condition/types/ha-automation-condition-device";
import { HaDeviceAction } from "./ha-automation-action-device_id";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import type { Condition } from "../../../../lovelace/common/validate-condition";
import "../ha-automation-action";
import "../../../../../components/ha-textfield";
import type { ActionElement } from "../ha-automation-action-row";
@customElement("ha-automation-action-if")
export class HaIfAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public action!: IfAction;
public static get defaultConfig() {
return {
if: [{ ...HaDeviceCondition.defaultConfig, condition: "device" }],
then: [HaDeviceAction.defaultConfig],
};
}
protected render() {
const action = this.action;
return html`
<h3>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.if.if"
)}*:
</h3>
<ha-automation-condition
.conditions=${action.if}
.hass=${this.hass}
@value-changed=${this._ifChanged}
></ha-automation-condition>
<h3>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.if.then"
)}*:
</h3>
<ha-automation-action
.actions=${action.then}
@value-changed=${this._thenChanged}
.hass=${this.hass}
></ha-automation-action>
<h3>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.if.else"
)}:
</h3>
<ha-automation-action
.actions=${action.else || []}
@value-changed=${this._elseChanged}
.hass=${this.hass}
></ha-automation-action>
`;
}
private _ifChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Condition[];
fireEvent(this, "value-changed", {
value: {
...this.action,
if: value,
},
});
}
private _thenChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Action[];
fireEvent(this, "value-changed", {
value: {
...this.action,
then: value,
},
});
}
private _elseChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Action[];
fireEvent(this, "value-changed", {
value: {
...this.action,
else: value,
},
});
}
static get styles(): CSSResultGroup {
return haStyle;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-if": HaIfAction;
}
}

View File

@@ -0,0 +1,71 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-textfield";
import { StopAction } from "../../../../../data/script";
import { HomeAssistant } from "../../../../../types";
import { ActionElement } from "../ha-automation-action-row";
@customElement("ha-automation-action-stop")
export class HaStopAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public action!: StopAction;
public static get defaultConfig() {
return { stop: "" };
}
protected render() {
const { error, stop } = this.action;
return html`
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.stop.stop"
)}
.value=${stop}
@change=${this._stopChanged}
></ha-textfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.stop.error"
)}
>
<ha-switch
.checked=${error ?? false}
@change=${this._errorChanged}
></ha-switch>
</ha-formfield>
`;
}
private _stopChanged(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.action, stop: (ev.target as any).value },
});
}
private _errorChanged(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: { ...this.action, error: (ev.target as any).checked },
});
}
static get styles(): CSSResultGroup {
return css`
ha-textfield {
display: block;
margin-bottom: 24px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-stop": HaStopAction;
}
}

View File

@@ -188,7 +188,12 @@ export class HaBlueprintAutomationEditor extends LitElement {
([key, value]) =>
html`<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">${value?.name || key}</span>
<span slot="description">${value?.description}</span>
<ha-markdown
slot="description"
class="card-content"
breaks
.content=${value?.description}
></ha-markdown>
${value?.selector
? html`<ha-selector
.hass=${this.hass}

View File

@@ -8,6 +8,7 @@ import "../../../components/ha-card";
import "../../../components/ha-textarea";
import "../../../components/ha-textfield";
import {
AUTOMATION_DEFAULT_MODE,
Condition,
ManualAutomationConfig,
Trigger,
@@ -99,7 +100,7 @@ export class HaManualAutomationEditor extends LitElement {
.label=${this.hass.localize(
"ui.panel.config.automation.editor.modes.label"
)}
.value=${this.config.mode}
.value=${this.config.mode || AUTOMATION_DEFAULT_MODE}
@selected=${this._modeChanged}
fixedMenuPosition
>

View File

@@ -442,6 +442,9 @@ export default class HaAutomationTriggerRow extends LitElement {
z-index: 3;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.rtl .card-menu {
float: left;
}
.triggered {
cursor: pointer;
position: absolute;
@@ -470,9 +473,6 @@ export default class HaAutomationTriggerRow extends LitElement {
background-color: var(--accent-color);
color: var(--text-accent-color, var(--text-primary-color));
}
.rtl .card-menu {
float: left;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}

View File

@@ -1,6 +1,7 @@
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import {
array,
assert,
assign,
literal,
@@ -10,6 +11,7 @@ import {
union,
} from "superstruct";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../../../../common/ensure-array";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template";
import { StateTrigger } from "../../../../../data/automation";
@@ -24,7 +26,7 @@ const stateTriggerStruct = assign(
baseTriggerStruct,
object({
platform: literal("state"),
entity_id: optional(string()),
entity_id: optional(union([string(), array(string())])),
attribute: optional(string()),
from: optional(string()),
to: optional(string()),
@@ -39,11 +41,15 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
@property() public trigger!: StateTrigger;
public static get defaultConfig() {
return { entity_id: "" };
return { entity_id: [] };
}
private _schema = memoizeOne((entityId) => [
{ name: "entity_id", required: true, selector: { entity: {} } },
{
name: "entity_id",
required: true,
selector: { entity: { multiple: true } },
},
{
name: "attribute",
selector: { attribute: { entity_id: entityId } },
@@ -85,7 +91,11 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
protected render() {
const trgFor = createDurationData(this.trigger.for);
const data = { ...this.trigger, ...{ for: trgFor } };
const data = {
...this.trigger,
entity_id: ensureArray(this.trigger.entity_id),
for: trgFor,
};
const schema = this._schema(this.trigger.entity_id);
return html`

View File

@@ -34,7 +34,6 @@ import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage-data-table";
import { HomeAssistant, Route } from "../../../types";
import { fileDownload } from "../../../util/file_download";
import { configSections } from "../ha-panel-config";
@customElement("ha-config-backup")
class HaConfigBackup extends LitElement {
@@ -129,13 +128,15 @@ class HaConfigBackup extends LitElement {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
back-path="/config/system"
.route=${this.route}
.tabs=${configSections.backup}
.columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._getItems(this._backupData.backups)}
.noDataText=${this.hass.localize("ui.panel.config.backup.no_bakcups")}
>
<span slot="header"
>${this.hass.localize("ui.panel.config.backup.caption")}</span
>
<ha-fab
slot="fab"
?disabled=${this._backupData.backing_up}

View File

@@ -38,7 +38,7 @@ class ConfigAnalytics extends LitElement {
: undefined;
return html`
<ha-card header="Analytics">
<ha-card outlined>
<div class="card-content">
${error ? html`<div class="error">${error}</div>` : ""}
<p>

View File

@@ -3,11 +3,11 @@ import "@polymer/app-layout/app-toolbar/app-toolbar";
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 "../../../layouts/hass-subpage";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../styles/polymer-ha-style";
import { configSections } from "../ha-panel-config";
import "./ha-config-section-core";
import "./ha-config-core-form";
import "./ha-config-name-form";
/*
* @appliesMixin LocalizeMixin
@@ -17,36 +17,29 @@ class HaConfigCore extends LocalizeMixin(PolymerElement) {
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);
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
.narrow .border {
max-width: 640px;
ha-config-name-form,
ha-config-core-form {
display: block;
margin-top: 24px;
}
</style>
<hass-tabs-subpage
<hass-subpage
hass="[[hass]]"
narrow="[[narrow]]"
route="[[route]]"
back-path="/config"
tabs="[[_computeTabs()]]"
show-advanced="[[showAdvanced]]"
header="[[localize('ui.panel.config.core.caption')]]"
back-path="/config/system"
>
<div class$="[[computeClasses(isWide)]]">
<ha-config-section-core
is-wide="[[isWide]]"
show-advanced="[[showAdvanced]]"
hass="[[hass]]"
></ha-config-section-core>
<div class="content">
<ha-config-name-form hass="[[hass]]"></ha-config-name-form>
<ha-config-core-form hass="[[hass]]"></ha-config-core-form>
</div>
</hass-tabs-subpage>
</hass-subpage>
`;
}
@@ -59,14 +52,6 @@ class HaConfigCore extends LocalizeMixin(PolymerElement) {
route: Object,
};
}
_computeTabs() {
return configSections.general;
}
computeClasses(isWide) {
return isWide ? "content" : "content narrow";
}
}
customElements.define("ha-config-core", HaConfigCore);

View File

@@ -9,6 +9,7 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-alert";
import "../../../components/ha-card";
import "../../../components/ha-checkbox";
import "../../../components/ha-network";
@@ -28,7 +29,7 @@ class ConfigNetwork extends LitElement {
@state() private _networkConfig?: NetworkConfig;
@state() private _error?: string;
@state() private _error?: { code: string; message: string };
protected render(): TemplateResult {
if (
@@ -39,9 +40,15 @@ class ConfigNetwork extends LitElement {
}
return html`
<ha-card header="Network">
<ha-card outlined header="Network">
<div class="card-content">
${this._error ? html`<div class="error">${this._error}</div>` : ""}
${this._error
? html`
<ha-alert alert-type="error"
>${this._error.message || this._error.code}</ha-alert
>
`
: ""}
<p>
Configure which network adapters integrations will use. Currently
this setting only affects multicast traffic. A restart is required

View File

@@ -0,0 +1,43 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../types";
import "./ha-config-analytics";
@customElement("ha-config-section-analytics")
class HaConfigSectionAnalytics extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
protected render(): TemplateResult {
return html`
<hass-subpage
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.analytics.caption")}
>
<div class="content">
<ha-config-analytics .hass=${this.hass}></ha-config-analytics>
</div>
</hass-subpage>
`;
}
static styles = css`
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-section-analytics": HaConfigSectionAnalytics;
}
}

View File

@@ -1,70 +0,0 @@
import "@material/mwc-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
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-analytics";
import "./ha-config-core-form";
import "./ha-config-name-form";
import "./ha-config-network";
import "./ha-config-url-form";
/*
* @appliesMixin LocalizeMixin
*/
class HaConfigSectionCore extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<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-network hass="[[hass]]"></ha-config-network>
<ha-config-analytics hass="[[hass]]"></ha-config-analytics>
</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,
};
}
}
customElements.define("ha-config-section-core", HaConfigSectionCore);

View File

@@ -0,0 +1,49 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../types";
import "./ha-config-network";
import "./ha-config-url-form";
@customElement("ha-config-section-network")
class HaConfigSectionNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
protected render(): TemplateResult {
return html`
<hass-subpage
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.network.caption")}
>
<div class="content">
<ha-config-url-form .hass=${this.hass}></ha-config-url-form>
<ha-config-network .hass=${this.hass}></ha-config-network>
</div>
</hass-subpage>
`;
}
static styles = css`
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
ha-config-network {
display: block;
margin-top: 24px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-section-network": HaConfigSectionNetwork;
}
}

View File

@@ -0,0 +1,40 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../types";
import "./ha-config-analytics";
@customElement("ha-config-section-storage")
class HaConfigSectionStorage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
protected render(): TemplateResult {
return html`
<hass-subpage
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
>
<div class="content"></div>
</hass-subpage>
`;
}
static styles = css`
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-section-storage": HaConfigSectionStorage;
}
}

View File

@@ -0,0 +1,115 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-card";
import "../../../components/ha-navigation-list";
import { CloudStatus } from "../../../data/cloud";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
@customElement("ha-config-system-navigation")
class HaConfigSystemNavigation extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true })
public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property({ attribute: false }) public cloudStatus?: CloudStatus;
@property({ type: Boolean }) public showAdvanced!: boolean;
protected render(): TemplateResult {
const pages = configSections.general.map((page) => ({
...page,
name: page.translationKey
? this.hass.localize(page.translationKey)
: page.name,
}));
return html`
<hass-subpage
back-path="/config"
.header=${this.hass.localize("ui.panel.config.dashboard.system.title")}
>
<ha-config-section
.narrow=${this.narrow}
.isWide=${this.isWide}
full-width
>
<ha-card>
${this.narrow
? html`<div class="title">
${this.hass.localize(
"ui.panel.config.dashboard.system.title"
)}
</div>`
: ""}
<ha-navigation-list
.hass=${this.hass}
.narrow=${this.narrow}
.pages=${pages}
></ha-navigation-list>
</ha-card>
</ha-config-section>
</hass-subpage>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
ha-card {
margin-bottom: env(safe-area-inset-bottom);
}
:host(:not([narrow])) ha-card {
margin-bottom: max(24px, env(safe-area-inset-bottom));
}
ha-config-section {
margin: auto;
margin-top: -32px;
max-width: 600px;
}
ha-card {
overflow: hidden;
}
ha-card a {
text-decoration: none;
color: var(--primary-text-color);
}
.title {
font-size: 16px;
padding: 16px;
padding-bottom: 0;
}
:host([narrow]) ha-card {
border-radius: 0;
box-shadow: unset;
}
:host([narrow]) ha-config-section {
margin-top: -42px;
}
ha-navigation-list {
--navigation-list-item-title-font-size: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-system-navigation": HaConfigSystemNavigation;
}
}

View File

@@ -9,17 +9,17 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-card";
import "../../../components/ha-switch";
import { isIPAddress } from "../../../common/string/is_ip_address";
import "../../../components/ha-alert";
import "../../../components/ha-card";
import "../../../components/ha-formfield";
import "../../../components/ha-switch";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import { CloudStatus, fetchCloudStatus } from "../../../data/cloud";
import { saveCoreConfig } from "../../../data/core";
import type { PolymerChangedEvent } from "../../../polymer-types";
import type { HomeAssistant } from "../../../types";
import { isIPAddress } from "../../../common/string/is_ip_address";
@customElement("ha-config-url-form")
class ConfigUrlForm extends LitElement {
@@ -74,7 +74,10 @@ class ConfigUrlForm extends LitElement {
}
return html`
<ha-card .header=${this.hass.localize("ui.panel.config.url.caption")}>
<ha-card
outlined
.header=${this.hass.localize("ui.panel.config.url.caption")}
>
<div class="card-content">
${!canEdit
? html`
@@ -335,6 +338,7 @@ class ConfigUrlForm extends LitElement {
a {
color: var(--primary-color);
text-decoration: none;
}
`;
}

View File

@@ -34,6 +34,7 @@ import { updateCanInstall, UpdateEntity } from "../../../data/update";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
import "../../../layouts/ha-app-layout";
import { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
@@ -119,10 +120,26 @@ class HaConfigDashboard extends LitElement {
private _notifyUpdates = false;
private _pages = memoizeOne((clouStatus, isLoaded) => {
const pages: PageNavigation[] = [];
if (clouStatus && isLoaded) {
pages.push({
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: this.cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
});
}
return [...pages, ...configSections.dashboard];
});
protected render(): TemplateResult {
const canInstallUpdates = this._filterUpdateEntitiesWithInstall(
this.hass.states
);
return html`
<ha-app-layout>
<app-header fixed slot="header">
@@ -175,30 +192,14 @@ class HaConfigDashboard extends LitElement {
${this.hass.localize("panel.config")}
</div>`
: ""}
${this.cloudStatus && isComponentLoaded(this.hass, "cloud")
? html`
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${[
{
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: this.cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
},
]}
></ha-config-navigation>
`
: ""}
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${configSections.dashboard}
.pages=${this._pages(
this.cloudStatus,
isComponentLoaded(this.hass, "cloud")
)}
></ha-config-navigation>
</ha-card>
<div class="tips">

View File

@@ -1,13 +1,14 @@
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { canShowPage } from "../../../common/config/can_show_page";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud";
import { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../../types";
import "../../../components/ha-navigation-list";
import type { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import type { HomeAssistant } from "../../../types";
@customElement("ha-config-navigation")
class HaConfigNavigation extends LitElement {
@@ -15,129 +16,71 @@ class HaConfigNavigation extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@property() public showAdvanced!: boolean;
@property() public pages!: PageNavigation[];
@property({ attribute: false }) public pages!: PageNavigation[];
protected render(): TemplateResult {
const pages = this.pages
.filter((page) =>
page.path === "#external-app-configuration"
? this.hass.auth.external?.config.hasSettingsScreen
: canShowPage(this.hass, page)
)
.map((page) => ({
...page,
name:
page.name ||
this.hass.localize(
`ui.panel.config.dashboard.${page.translationKey}.title`
),
description:
page.component === "cloud" && (page.info as CloudStatus)
? page.info.logged_in
? `
${this.hass.localize(
"ui.panel.config.cloud.description_login",
"email",
(page.info as CloudStatusLoggedIn).email
)}
`
: `
${this.hass.localize(
"ui.panel.config.cloud.description_features"
)}
`
: `
${
page.description ||
this.hass.localize(
`ui.panel.config.dashboard.${page.translationKey}.description`
)
}
`,
}));
return html`
${this.pages.map((page) =>
(
page.path === "#external-app-configuration"
? this.hass.auth.external?.config.hasSettingsScreen
: canShowPage(this.hass, page)
)
? html`
<a href=${page.path} role="option" tabindex="-1">
<paper-icon-item @click=${this._entryClicked}>
<div
class=${page.iconColor ? "icon-background" : ""}
slot="item-icon"
.style="background-color: ${page.iconColor || "undefined"}"
>
<ha-svg-icon .path=${page.iconPath}></ha-svg-icon>
</div>
<paper-item-body two-line>
${page.name ||
this.hass.localize(
`ui.panel.config.dashboard.${page.translationKey}.title`
)}
${page.component === "cloud" && (page.info as CloudStatus)
? page.info.logged_in
? html`
<div secondary>
${this.hass.localize(
"ui.panel.config.cloud.description_login",
"email",
(page.info as CloudStatusLoggedIn).email
)}
</div>
`
: html`
<div secondary>
${this.hass.localize(
"ui.panel.config.cloud.description_features"
)}
</div>
`
: html`
<div secondary>
${page.description ||
this.hass.localize(
`ui.panel.config.dashboard.${page.translationKey}.description`
)}
</div>
`}
</paper-item-body>
${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""}
</paper-icon-item>
</a>
`
: ""
)}
<ha-navigation-list
hasSecondary
.hass=${this.hass}
.narrow=${this.narrow}
.pages=${pages}
@click=${this._entryClicked}
></ha-navigation-list>
`;
}
private _entryClicked(ev) {
ev.currentTarget.blur();
if (
ev.currentTarget.parentElement.href.endsWith(
"#external-app-configuration"
)
) {
const anchor = ev
.composedPath()
.find((n) => (n as HTMLElement).tagName === "A") as
| HTMLAnchorElement
| undefined;
if (anchor?.href?.endsWith("#external-app-configuration")) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
}
static get styles(): CSSResultGroup {
return css`
a {
text-decoration: none;
color: var(--primary-text-color);
position: relative;
display: block;
outline: 0;
}
ha-svg-icon,
ha-icon-next {
color: var(--secondary-text-color);
height: 24px;
width: 24px;
}
ha-svg-icon {
padding: 8px;
}
.iron-selected paper-item::before,
a:not(.iron-selected):focus::before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
content: "";
transition: opacity 15ms linear;
will-change: opacity;
}
a:not(.iron-selected):focus::before {
background-color: currentColor;
opacity: var(--dark-divider-opacity);
}
.iron-selected paper-item:focus::before,
.iron-selected:focus paper-item::before {
opacity: 0.2;
}
.icon-background {
border-radius: 50%;
}
.icon-background ha-svg-icon {
color: #fff;
}
`;
}
}
declare global {

View File

@@ -11,10 +11,13 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeDomain } from "../../../common/entity/compute_domain";
import { domainIcon } from "../../../common/entity/domain_icon";
import { stringCompare } from "../../../common/string/compare";
import { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-alert";
import "../../../components/ha-area-picker";
import "../../../components/ha-expansion-panel";
@@ -96,7 +99,7 @@ const OVERRIDE_SENSOR_UNITS = {
pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"],
};
const SWITCH_AS_DOMAINS = ["light", "lock", "cover", "fan", "siren"];
const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren"];
@customElement("entity-registry-settings")
export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@@ -273,22 +276,29 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@selected=${this._deviceClassChanged}
@closed=${stopPropagation}
>
${this._deviceClassOptions[0].map(
(deviceClass: string) => html`
<mwc-list-item .value=${deviceClass}>
${this.hass.localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
)}
${this._deviceClassesSorted(
domain,
this._deviceClassOptions[0],
this.hass.localize
).map(
(entry) => html`
<mwc-list-item .value=${entry.deviceClass}>
${entry.label}
</mwc-list-item>
`
)}
<li divider role="separator"></li>
${this._deviceClassOptions[1].map(
(deviceClass: string) => html`
<mwc-list-item .value=${deviceClass}>
${this.hass.localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
)}
${this._deviceClassOptions[0].length &&
this._deviceClassOptions[1].length
? html`<li divider role="separator"></li>`
: ""}
${this._deviceClassesSorted(
domain,
this._deviceClassOptions[1],
this.hass.localize
).map(
(entry) => html`
<mwc-list-item .value=${entry.deviceClass}>
${entry.label}
</mwc-list-item>
`
)}
@@ -296,9 +306,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
`
: ""}
${this._deviceClass &&
stateObj.attributes.unit_of_measurement &&
stateObj?.attributes.unit_of_measurement &&
OVERRIDE_SENSOR_UNITS[this._deviceClass]?.includes(
stateObj.attributes.unit_of_measurement
stateObj?.attributes.unit_of_measurement
)
? html`
<ha-select
@@ -332,10 +342,14 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
<mwc-list-item value="switch" selected>
${domainToName(this.hass.localize, "switch")}</mwc-list-item
>
${SWITCH_AS_DOMAINS.map(
(as_domain) => html`
<mwc-list-item .value=${as_domain}>
${domainToName(this.hass.localize, as_domain)}
<li divider role="separator"></li>
${this._switchAsDomainsSorted(
SWITCH_AS_DOMAINS,
this.hass.localize
).map(
(entry) => html`
<mwc-list-item .value=${entry.domain}>
${entry.label}
</mwc-list-item>
`
)}
@@ -716,9 +730,31 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
}
private async _showOptionsFlow() {
showOptionsFlowDialog(this, this._helperConfigEntry!);
showOptionsFlowDialog(this, this._helperConfigEntry!, null);
}
private _switchAsDomainsSorted = memoizeOne(
(domains: string[], localize: LocalizeFunc) =>
domains
.map((entry) => ({
domain: entry,
label: domainToName(localize, entry),
}))
.sort((a, b) => stringCompare(a.label, b.label))
);
private _deviceClassesSorted = memoizeOne(
(domain: string, deviceClasses: string[], localize: LocalizeFunc) =>
deviceClasses
.map((entry) => ({
deviceClass: entry,
label: localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${entry}`
),
}))
.sort((a, b) => stringCompare(a.label, b.label))
);
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@@ -384,7 +384,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
for (const entry of filteredEntities) {
const entity = this.hass.states[entry.entity_id];
const unavailable = entity?.state === UNAVAILABLE;
const restored = entity?.attributes.restored;
const restored = entity?.attributes.restored === true;
const areaId = entry.area_id ?? deviceLookup[entry.device_id!]?.area_id;
const area = areaId ? areaLookup[areaId] : undefined;

View File

@@ -4,12 +4,15 @@ import {
mdiBadgeAccountHorizontal,
mdiCellphoneCog,
mdiCog,
mdiCpu32Bit,
mdiDevices,
mdiHomeAssistant,
mdiInformation,
mdiInformationOutline,
mdiLightningBolt,
mdiMapMarkerRadius,
mdiMathLog,
mdiNetwork,
mdiNfcVariant,
mdiPalette,
mdiPaletteSwatch,
@@ -20,6 +23,7 @@ import {
mdiShape,
mdiSofa,
mdiTools,
mdiUpdate,
mdiViewDashboard,
} from "@mdi/js";
import { PolymerElement } from "@polymer/polymer";
@@ -58,11 +62,11 @@ export const configSections: { [name: string]: PageNavigation[] } = {
core: true,
},
{
path: "/config/blueprint",
translationKey: "blueprints",
iconPath: mdiPaletteSwatch,
iconColor: "#64B5F6",
component: "blueprint",
path: "/config/areas",
translationKey: "areas",
iconPath: mdiSofa,
iconColor: "#E48629",
components: ["zone"],
},
{
path: "/config/backup",
@@ -74,8 +78,8 @@ export const configSections: { [name: string]: PageNavigation[] } = {
{
path: "/hassio",
translationKey: "supervisor",
iconPath: mdiHomeAssistant,
iconColor: "#4084CD",
iconPath: mdiPuzzle,
iconColor: "#F1C447",
component: "hassio",
},
{
@@ -97,7 +101,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
translationKey: "people",
iconPath: mdiAccount,
iconColor: "#E48629",
components: ["person", "zone", "users"],
components: ["person", "users"],
},
{
path: "#external-app-configuration",
@@ -106,9 +110,16 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconColor: "#8E24AA",
},
{
path: "/config/server_control",
translationKey: "settings",
path: "/config/system",
translationKey: "system",
iconPath: mdiCog,
iconColor: "#301ABE",
core: true,
},
{
path: "/config/info",
translationKey: "about",
iconPath: mdiInformationOutline,
iconColor: "#4A5963",
core: true,
},
@@ -148,11 +159,11 @@ export const configSections: { [name: string]: PageNavigation[] } = {
core: true,
},
{
component: "areas",
path: "/config/areas",
translationKey: "ui.panel.config.areas.caption",
iconPath: mdiSofa,
iconColor: "#2D338F",
component: "helpers",
path: "/config/helpers",
translationKey: "ui.panel.config.helpers.caption",
iconPath: mdiTools,
iconColor: "#4D2EA4",
core: true,
},
],
@@ -178,16 +189,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconPath: mdiScriptText,
iconColor: "#518C43",
},
{
component: "helpers",
path: "/config/helpers",
translationKey: "ui.panel.config.helpers.caption",
iconPath: mdiTools,
iconColor: "#4D2EA4",
core: true,
},
],
blueprints: [
{
component: "blueprint",
path: "/config/blueprint",
@@ -232,13 +233,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconPath: mdiAccount,
iconColor: "#E48629",
},
{
component: "zone",
path: "/config/zone",
translationKey: "ui.panel.config.zone.caption",
iconPath: mdiMapMarkerRadius,
iconColor: "#E48629",
},
{
component: "users",
path: "/config/users",
@@ -249,6 +243,23 @@ export const configSections: { [name: string]: PageNavigation[] } = {
advancedOnly: true,
},
],
areas: [
{
component: "areas",
path: "/config/areas",
translationKey: "ui.panel.config.areas.caption",
iconPath: mdiSofa,
iconColor: "#2D338F",
core: true,
},
{
component: "zone",
path: "/config/zone",
translationKey: "ui.panel.config.zone.caption",
iconPath: mdiMapMarkerRadius,
iconColor: "#E48629",
},
],
general: [
{
component: "core",
@@ -274,6 +285,45 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconColor: "#4A5963",
core: true,
},
{
path: "/config/backup",
translationKey: "ui.panel.config.backup.caption",
iconPath: mdiBackupRestore,
iconColor: "#4084CD",
component: "backup",
},
{
path: "/config/analytics",
translationKey: "ui.panel.config.analytics.caption",
iconPath: mdiShape,
iconColor: "#f1c447",
},
{
path: "/config/hardware",
translationKey: "ui.panel.config.hardware.caption",
iconPath: mdiCpu32Bit,
iconColor: "#4A5963",
},
{
path: "/config/network",
translationKey: "ui.panel.config.network.caption",
iconPath: mdiNetwork,
iconColor: "#B1345C",
},
{
path: "/config/storage",
translationKey: "ui.panel.config.storage.caption",
iconPath: mdiServer,
iconColor: "#518C43",
},
{
path: "/config/update",
translationKey: "ui.panel.config.updates.caption",
iconPath: mdiUpdate,
iconColor: "#4A5963",
},
],
about: [
{
component: "info",
path: "/config/info",
@@ -296,6 +346,10 @@ class HaPanelConfig extends HassRouterPage {
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
routes: {
analytics: {
tag: "ha-config-section-analytics",
load: () => import("./core/ha-config-section-analytics"),
},
areas: {
tag: "ha-config-areas",
load: () => import("./areas/ha-config-areas"),
@@ -328,9 +382,9 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-devices",
load: () => import("./devices/ha-config-devices"),
},
server_control: {
tag: "ha-config-server-control",
load: () => import("./server_control/ha-config-server-control"),
system: {
tag: "ha-config-system-navigation",
load: () => import("./core/ha-config-system-navigation"),
},
logs: {
tag: "ha-config-logs",
@@ -362,6 +416,10 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-lovelace",
load: () => import("./lovelace/ha-config-lovelace"),
},
network: {
tag: "ha-config-section-network",
load: () => import("./core/ha-config-section-network"),
},
person: {
tag: "ha-config-person",
load: () => import("./person/ha-config-person"),
@@ -378,6 +436,14 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-helpers",
load: () => import("./helpers/ha-config-helpers"),
},
server_control: {
tag: "ha-config-server-control",
load: () => import("./server_control/ha-config-server-control"),
},
storage: {
tag: "ha-config-section-storage",
load: () => import("./core/ha-config-section-storage"),
},
users: {
tag: "ha-config-users",
load: () => import("./users/ha-config-users"),

View File

@@ -6,21 +6,29 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { domainIcon } from "../../../common/entity/domain_icon";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
import { extractSearchParam } from "../../../common/url/search-params";
import {
DataTableColumnContainer,
RowClickedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-fab";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-icon";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-svg-icon";
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
import { getConfigFlowHandlers } from "../../../data/config_flow";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../data/entity_registry";
import { domainToName } from "../../../data/integration";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage-data-table";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
@@ -29,14 +37,6 @@ import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor";
import { configSections } from "../ha-panel-config";
import { HELPER_DOMAINS } from "./const";
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
import { navigate } from "../../../common/navigate";
import { extractSearchParam } from "../../../common/url/search-params";
import { getConfigFlowHandlers } from "../../../data/config_flow";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
// This groups items by a key but only returns last entry per key.
const groupByOne = <T>(
@@ -196,7 +196,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.automations}
.tabs=${configSections.devices}
.columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._getItems(
this._stateItems,

View File

@@ -1,11 +1,10 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property } from "lit/decorators";
import "../../../layouts/hass-tabs-subpage";
import "../../../components/ha-logo-svg";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { configSections } from "../ha-panel-config";
import "./integrations-card";
import "./system-health-card";
@@ -29,12 +28,11 @@ class HaConfigInfo extends LitElement {
(window as any).CUSTOM_UI_LIST || [];
return html`
<hass-tabs-subpage
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.general}
.header=${this.hass.localize("ui.panel.config.info.caption")}
>
<div class="about">
<a
@@ -113,21 +111,23 @@ class HaConfigInfo extends LitElement {
"type",
JS_TYPE
)}
${customUiList.length > 0
? html`
<div>
${this.hass.localize("ui.panel.config.info.custom_uis")}
${customUiList.map(
(item) => html`
<div>
<a href=${item.url} target="_blank"> ${item.name}</a>:
${item.version}
</div>
`
)}
</div>
`
: ""}
${
customUiList.length > 0
? html`
<div>
${this.hass.localize("ui.panel.config.info.custom_uis")}
${customUiList.map(
(item) => html`
<div>
<a href=${item.url} target="_blank"> ${item.name}</a
>: ${item.version}
</div>
`
)}
</div>
`
: ""
}
</p>
</div>
<div>

View File

@@ -700,6 +700,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
this._handleFlowUpdated();
},
startFlowHandler: domain,
manifest: this._manifests[domain],
showAdvanced: this.hass.userData?.showAdvanced,
});
}

View File

@@ -482,7 +482,11 @@ export class HaIntegrationCard extends LitElement {
);
private _showOptions(ev) {
showOptionsFlowDialog(this, ev.target.closest("ha-card").configEntry);
showOptionsFlowDialog(
this,
ev.target.closest("ha-card").configEntry,
this.manifest
);
}
private _handleRename(ev: CustomEvent<RequestSelectedDetail>): void {

Some files were not shown because too many files have changed in this diff Show More