mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-07 20:26:30 +00:00
Compare commits
47 Commits
20220425.0
...
Add-Redire
Author | SHA1 | Date | |
---|---|---|---|
![]() |
191f81d9fe | ||
![]() |
2751f8f33b | ||
![]() |
57f2df3b3e | ||
![]() |
6822f0d067 | ||
![]() |
cfba957313 | ||
![]() |
3149ffbf19 | ||
![]() |
4cd8b76d7e | ||
![]() |
4b644d8bc5 | ||
![]() |
307cd5ad8c | ||
![]() |
ebc807a6a4 | ||
![]() |
66adecdfc9 | ||
![]() |
2cc6432a0f | ||
![]() |
a2c0c0474a | ||
![]() |
27884b9a54 | ||
![]() |
293df61872 | ||
![]() |
f82dada3e5 | ||
![]() |
e5824c4794 | ||
![]() |
186550229c | ||
![]() |
7877dd8e6b | ||
![]() |
b03abc249b | ||
![]() |
fda03918b9 | ||
![]() |
6747375a1b | ||
![]() |
53b6e31881 | ||
![]() |
fa004de2d1 | ||
![]() |
3605f7b70f | ||
![]() |
5348c54c91 | ||
![]() |
684e4421bc | ||
![]() |
28f5611df5 | ||
![]() |
8da73d49d7 | ||
![]() |
049ddd5f84 | ||
![]() |
8ae2d4e93a | ||
![]() |
824bb9ba35 | ||
![]() |
d550b1a18e | ||
![]() |
dea6c0e761 | ||
![]() |
9caee357c0 | ||
![]() |
35d892c418 | ||
![]() |
9572a2a46b | ||
![]() |
8996361b26 | ||
![]() |
02ee731602 | ||
![]() |
bb1e6bf35b | ||
![]() |
c1b65285c1 | ||
![]() |
8b8d6e5fa3 | ||
![]() |
c34fe184e8 | ||
![]() |
7363838f86 | ||
![]() |
3081425ccd | ||
![]() |
95d494a54c | ||
![]() |
145e5d7bc6 |
@@ -2,6 +2,7 @@ import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-ansi-to-html";
|
||||
import "../../../../src/components/ha-card";
|
||||
import {
|
||||
fetchHassioAddonLogs,
|
||||
@@ -11,7 +12,6 @@ import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/hassio-ansi-to-html";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-addon-logs")
|
||||
@@ -40,9 +40,9 @@ class HassioAddonLogs extends LitElement {
|
||||
: ""}
|
||||
<div class="card-content">
|
||||
${this._content
|
||||
? html`<hassio-ansi-to-html
|
||||
? html`<ha-ansi-to-html
|
||||
.content=${this._content}
|
||||
></hassio-ansi-to-html>`
|
||||
></ha-ansi-to-html>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import "@material/mwc-button";
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
|
||||
import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -166,7 +166,15 @@ export class HassioBackups extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.tabs=${supervisorTabs(this.hass)}
|
||||
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
|
||||
? [
|
||||
{
|
||||
translationKey: "panel.backups",
|
||||
path: `/hassio/backups`,
|
||||
iconPath: mdiBackupRestore,
|
||||
},
|
||||
]
|
||||
: supervisorTabs(this.hass)}
|
||||
.hass=${this.hass}
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.searchLabel=${this.supervisor.localize("search")}
|
||||
@@ -182,7 +190,9 @@ export class HassioBackups extends LitElement {
|
||||
selectable
|
||||
hasFab
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path="/config"
|
||||
back-path=${atLeastVersion(this.hass.config.version, 2022, 5)
|
||||
? "/config/system"
|
||||
: "/config"}
|
||||
supervisor
|
||||
>
|
||||
<ha-button-menu
|
||||
|
@@ -3,8 +3,8 @@ import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../src/common/config/version";
|
||||
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../src/common/dom/fire_event";
|
||||
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
|
||||
import { mainWindow } from "../../src/common/dom/get_main_window";
|
||||
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
@@ -73,6 +73,14 @@ export class HassioMain extends SupervisorBaseElement {
|
||||
});
|
||||
});
|
||||
|
||||
// Forward keydown events to the main window for quickbar access
|
||||
document.body.addEventListener("keydown", (ev) => {
|
||||
// @ts-ignore
|
||||
fireEvent(mainWindow, "hass-quick-bar-trigger", ev, {
|
||||
bubbles: false,
|
||||
});
|
||||
});
|
||||
|
||||
makeDialogManager(this, this.shadowRoot!);
|
||||
}
|
||||
|
||||
|
@@ -15,7 +15,7 @@ import {
|
||||
} from "../../src/panels/my/ha-panel-my";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
|
||||
const REDIRECTS: Redirects = {
|
||||
export const REDIRECTS: Redirects = {
|
||||
supervisor: {
|
||||
redirect: "/hassio/dashboard",
|
||||
},
|
||||
|
@@ -23,6 +23,10 @@ import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
UNHEALTHY_REASON_URL,
|
||||
UNSUPPORTED_REASON_URL,
|
||||
} from "../../../src/panels/config/system-health/ha-config-system-health";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { bytesToString } from "../../../src/util/bytes-to-string";
|
||||
@@ -30,11 +34,6 @@ import { documentationUrl } from "../../../src/util/documentation-url";
|
||||
import "../components/supervisor-metric";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
const UNSUPPORTED_REASON_URL = {};
|
||||
const UNHEALTHY_REASON_URL = {
|
||||
privileged: "/more-info/unsupported/privileged",
|
||||
};
|
||||
|
||||
@customElement("hassio-supervisor-info")
|
||||
class HassioSupervisorInfo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import "../../../src/components/ha-ansi-to-html";
|
||||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -11,7 +12,6 @@ import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import "../components/hassio-ansi-to-html";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
interface LogProvider {
|
||||
@@ -89,8 +89,8 @@ class HassioSupervisorLog extends LitElement {
|
||||
|
||||
<div class="card-content" id="content">
|
||||
${this._content
|
||||
? html`<hassio-ansi-to-html .content=${this._content}>
|
||||
</hassio-ansi-to-html>`
|
||||
? html`<ha-ansi-to-html .content=${this._content}>
|
||||
</ha-ansi-to-html>`
|
||||
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
|
@@ -106,6 +106,7 @@
|
||||
"deep-clone-simple": "^1.1.1",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"fuse.js": "^6.0.0",
|
||||
"fuzzysort": "^1.2.1",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^1.1.5",
|
||||
"home-assistant-js-websocket": "^7.0.3",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = home-assistant-frontend
|
||||
version = 20220425.0
|
||||
version = 20220427.0
|
||||
author = The Home Assistant Authors
|
||||
author_email = hello@home-assistant.io
|
||||
license = Apache-2.0
|
||||
|
16
src/common/datetime/duration.ts
Normal file
16
src/common/datetime/duration.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import secondsToDuration from "./seconds_to_duration";
|
||||
|
||||
const DAY_IN_SECONDS = 86400;
|
||||
const HOUR_IN_SECONDS = 3600;
|
||||
const MINUTE_IN_SECONDS = 60;
|
||||
|
||||
export const UNIT_TO_SECOND_CONVERT = {
|
||||
s: 1,
|
||||
min: MINUTE_IN_SECONDS,
|
||||
h: HOUR_IN_SECONDS,
|
||||
d: DAY_IN_SECONDS,
|
||||
};
|
||||
|
||||
export const formatDuration = (duration: string, units: string): string =>
|
||||
secondsToDuration(parseFloat(duration) * UNIT_TO_SECOND_CONVERT[units]) ||
|
||||
"0";
|
@@ -13,6 +13,7 @@ import { formatNumber, isNumericState } from "../number/format_number";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { supportsFeature } from "./supports-feature";
|
||||
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
|
||||
|
||||
export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
@@ -28,6 +29,21 @@ export const computeStateDisplay = (
|
||||
|
||||
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
|
||||
if (isNumericState(stateObj)) {
|
||||
// state is duration
|
||||
if (
|
||||
stateObj.attributes.device_class === "duration" &&
|
||||
stateObj.attributes.unit_of_measurement &&
|
||||
UNIT_TO_SECOND_CONVERT[stateObj.attributes.unit_of_measurement]
|
||||
) {
|
||||
try {
|
||||
return formatDuration(
|
||||
compareState,
|
||||
stateObj.attributes.unit_of_measurement
|
||||
);
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
}
|
||||
}
|
||||
if (stateObj.attributes.device_class === "monetary") {
|
||||
try {
|
||||
return formatNumber(compareState, locale, {
|
||||
|
@@ -1,244 +0,0 @@
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2015 - present Microsoft Corporation
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
|
||||
|
||||
/**
|
||||
* An inlined enum containing useful character codes (to be used with String.charCodeAt).
|
||||
* Please leave the const keyword such that it gets inlined when compiled to JavaScript!
|
||||
*/
|
||||
export enum CharCode {
|
||||
Null = 0,
|
||||
/**
|
||||
* The `\b` character.
|
||||
*/
|
||||
Backspace = 8,
|
||||
/**
|
||||
* The `\t` character.
|
||||
*/
|
||||
Tab = 9,
|
||||
/**
|
||||
* The `\n` character.
|
||||
*/
|
||||
LineFeed = 10,
|
||||
/**
|
||||
* The `\r` character.
|
||||
*/
|
||||
CarriageReturn = 13,
|
||||
Space = 32,
|
||||
/**
|
||||
* The `!` character.
|
||||
*/
|
||||
ExclamationMark = 33,
|
||||
/**
|
||||
* The `"` character.
|
||||
*/
|
||||
DoubleQuote = 34,
|
||||
/**
|
||||
* The `#` character.
|
||||
*/
|
||||
Hash = 35,
|
||||
/**
|
||||
* The `$` character.
|
||||
*/
|
||||
DollarSign = 36,
|
||||
/**
|
||||
* The `%` character.
|
||||
*/
|
||||
PercentSign = 37,
|
||||
/**
|
||||
* The `&` character.
|
||||
*/
|
||||
Ampersand = 38,
|
||||
/**
|
||||
* The `'` character.
|
||||
*/
|
||||
SingleQuote = 39,
|
||||
/**
|
||||
* The `(` character.
|
||||
*/
|
||||
OpenParen = 40,
|
||||
/**
|
||||
* The `)` character.
|
||||
*/
|
||||
CloseParen = 41,
|
||||
/**
|
||||
* The `*` character.
|
||||
*/
|
||||
Asterisk = 42,
|
||||
/**
|
||||
* The `+` character.
|
||||
*/
|
||||
Plus = 43,
|
||||
/**
|
||||
* The `,` character.
|
||||
*/
|
||||
Comma = 44,
|
||||
/**
|
||||
* The `-` character.
|
||||
*/
|
||||
Dash = 45,
|
||||
/**
|
||||
* The `.` character.
|
||||
*/
|
||||
Period = 46,
|
||||
/**
|
||||
* The `/` character.
|
||||
*/
|
||||
Slash = 47,
|
||||
|
||||
Digit0 = 48,
|
||||
Digit1 = 49,
|
||||
Digit2 = 50,
|
||||
Digit3 = 51,
|
||||
Digit4 = 52,
|
||||
Digit5 = 53,
|
||||
Digit6 = 54,
|
||||
Digit7 = 55,
|
||||
Digit8 = 56,
|
||||
Digit9 = 57,
|
||||
|
||||
/**
|
||||
* The `:` character.
|
||||
*/
|
||||
Colon = 58,
|
||||
/**
|
||||
* The `;` character.
|
||||
*/
|
||||
Semicolon = 59,
|
||||
/**
|
||||
* The `<` character.
|
||||
*/
|
||||
LessThan = 60,
|
||||
/**
|
||||
* The `=` character.
|
||||
*/
|
||||
Equals = 61,
|
||||
/**
|
||||
* The `>` character.
|
||||
*/
|
||||
GreaterThan = 62,
|
||||
/**
|
||||
* The `?` character.
|
||||
*/
|
||||
QuestionMark = 63,
|
||||
/**
|
||||
* The `@` character.
|
||||
*/
|
||||
AtSign = 64,
|
||||
|
||||
A = 65,
|
||||
B = 66,
|
||||
C = 67,
|
||||
D = 68,
|
||||
E = 69,
|
||||
F = 70,
|
||||
G = 71,
|
||||
H = 72,
|
||||
I = 73,
|
||||
J = 74,
|
||||
K = 75,
|
||||
L = 76,
|
||||
M = 77,
|
||||
N = 78,
|
||||
O = 79,
|
||||
P = 80,
|
||||
Q = 81,
|
||||
R = 82,
|
||||
S = 83,
|
||||
T = 84,
|
||||
U = 85,
|
||||
V = 86,
|
||||
W = 87,
|
||||
X = 88,
|
||||
Y = 89,
|
||||
Z = 90,
|
||||
|
||||
/**
|
||||
* The `[` character.
|
||||
*/
|
||||
OpenSquareBracket = 91,
|
||||
/**
|
||||
* The `\` character.
|
||||
*/
|
||||
Backslash = 92,
|
||||
/**
|
||||
* The `]` character.
|
||||
*/
|
||||
CloseSquareBracket = 93,
|
||||
/**
|
||||
* The `^` character.
|
||||
*/
|
||||
Caret = 94,
|
||||
/**
|
||||
* The `_` character.
|
||||
*/
|
||||
Underline = 95,
|
||||
/**
|
||||
* The ``(`)`` character.
|
||||
*/
|
||||
BackTick = 96,
|
||||
|
||||
a = 97,
|
||||
b = 98,
|
||||
c = 99,
|
||||
d = 100,
|
||||
e = 101,
|
||||
f = 102,
|
||||
g = 103,
|
||||
h = 104,
|
||||
i = 105,
|
||||
j = 106,
|
||||
k = 107,
|
||||
l = 108,
|
||||
m = 109,
|
||||
n = 110,
|
||||
o = 111,
|
||||
p = 112,
|
||||
q = 113,
|
||||
r = 114,
|
||||
s = 115,
|
||||
t = 116,
|
||||
u = 117,
|
||||
v = 118,
|
||||
w = 119,
|
||||
x = 120,
|
||||
y = 121,
|
||||
z = 122,
|
||||
|
||||
/**
|
||||
* The `{` character.
|
||||
*/
|
||||
OpenCurlyBrace = 123,
|
||||
/**
|
||||
* The `|` character.
|
||||
*/
|
||||
Pipe = 124,
|
||||
/**
|
||||
* The `}` character.
|
||||
*/
|
||||
CloseCurlyBrace = 125,
|
||||
/**
|
||||
* The `~` character.
|
||||
*/
|
||||
Tilde = 126,
|
||||
}
|
@@ -1,551 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2015 - present Microsoft Corporation
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { CharCode } from "./char-code";
|
||||
|
||||
const _debug = false;
|
||||
|
||||
export interface Match {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
const _maxLen = 128;
|
||||
|
||||
function initTable() {
|
||||
const table: number[][] = [];
|
||||
const row: number[] = [];
|
||||
for (let i = 0; i <= _maxLen; i++) {
|
||||
row[i] = 0;
|
||||
}
|
||||
for (let i = 0; i <= _maxLen; i++) {
|
||||
table.push(row.slice(0));
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
function isSeparatorAtPos(value: string, index: number): boolean {
|
||||
if (index < 0 || index >= value.length) {
|
||||
return false;
|
||||
}
|
||||
const code = value.codePointAt(index);
|
||||
switch (code) {
|
||||
case CharCode.Underline:
|
||||
case CharCode.Dash:
|
||||
case CharCode.Period:
|
||||
case CharCode.Space:
|
||||
case CharCode.Slash:
|
||||
case CharCode.Backslash:
|
||||
case CharCode.SingleQuote:
|
||||
case CharCode.DoubleQuote:
|
||||
case CharCode.Colon:
|
||||
case CharCode.DollarSign:
|
||||
case CharCode.LessThan:
|
||||
case CharCode.OpenParen:
|
||||
case CharCode.OpenSquareBracket:
|
||||
return true;
|
||||
case undefined:
|
||||
return false;
|
||||
default:
|
||||
if (isEmojiImprecise(code)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isWhitespaceAtPos(value: string, index: number): boolean {
|
||||
if (index < 0 || index >= value.length) {
|
||||
return false;
|
||||
}
|
||||
const code = value.charCodeAt(index);
|
||||
switch (code) {
|
||||
case CharCode.Space:
|
||||
case CharCode.Tab:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
|
||||
return word[pos] !== wordLow[pos];
|
||||
}
|
||||
|
||||
export function isPatternInWord(
|
||||
patternLow: string,
|
||||
patternPos: number,
|
||||
patternLen: number,
|
||||
wordLow: string,
|
||||
wordPos: number,
|
||||
wordLen: number,
|
||||
fillMinWordPosArr = false
|
||||
): boolean {
|
||||
while (patternPos < patternLen && wordPos < wordLen) {
|
||||
if (patternLow[patternPos] === wordLow[wordPos]) {
|
||||
if (fillMinWordPosArr) {
|
||||
// Remember the min word position for each pattern position
|
||||
_minWordMatchPos[patternPos] = wordPos;
|
||||
}
|
||||
patternPos += 1;
|
||||
}
|
||||
wordPos += 1;
|
||||
}
|
||||
return patternPos === patternLen; // pattern must be exhausted
|
||||
}
|
||||
|
||||
enum Arrow {
|
||||
Diag = 1,
|
||||
Left = 2,
|
||||
LeftLeft = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* An array representing a fuzzy match.
|
||||
*
|
||||
* 0. the score
|
||||
* 1. the offset at which matching started
|
||||
* 2. `<match_pos_N>`
|
||||
* 3. `<match_pos_1>`
|
||||
* 4. `<match_pos_0>` etc
|
||||
*/
|
||||
// export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];// [number, number, number];
|
||||
export type FuzzyScore = Array<number>;
|
||||
|
||||
export function fuzzyScore(
|
||||
pattern: string,
|
||||
patternLow: string,
|
||||
patternStart: number,
|
||||
word: string,
|
||||
wordLow: string,
|
||||
wordStart: number,
|
||||
firstMatchCanBeWeak: boolean
|
||||
): FuzzyScore | undefined {
|
||||
const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
|
||||
const wordLen = word.length > _maxLen ? _maxLen : word.length;
|
||||
|
||||
if (
|
||||
patternStart >= patternLen ||
|
||||
wordStart >= wordLen ||
|
||||
patternLen - patternStart > wordLen - wordStart
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Run a simple check if the characters of pattern occur
|
||||
// (in order) at all in word. If that isn't the case we
|
||||
// stop because no match will be possible
|
||||
if (
|
||||
!isPatternInWord(
|
||||
patternLow,
|
||||
patternStart,
|
||||
patternLen,
|
||||
wordLow,
|
||||
wordStart,
|
||||
wordLen,
|
||||
true
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Find the max matching word position for each pattern position
|
||||
// NOTE: the min matching word position was filled in above, in the `isPatternInWord` call
|
||||
_fillInMaxWordMatchPos(
|
||||
patternLen,
|
||||
wordLen,
|
||||
patternStart,
|
||||
wordStart,
|
||||
patternLow,
|
||||
wordLow
|
||||
);
|
||||
|
||||
let row: number;
|
||||
let column = 1;
|
||||
let patternPos: number;
|
||||
let wordPos: number;
|
||||
|
||||
const hasStrongFirstMatch = [false];
|
||||
|
||||
// There will be a match, fill in tables
|
||||
for (
|
||||
row = 1, patternPos = patternStart;
|
||||
patternPos < patternLen;
|
||||
row++, patternPos++
|
||||
) {
|
||||
// Reduce search space to possible matching word positions and to possible access from next row
|
||||
const minWordMatchPos = _minWordMatchPos[patternPos];
|
||||
const maxWordMatchPos = _maxWordMatchPos[patternPos];
|
||||
const nextMaxWordMatchPos =
|
||||
patternPos + 1 < patternLen ? _maxWordMatchPos[patternPos + 1] : wordLen;
|
||||
|
||||
for (
|
||||
column = minWordMatchPos - wordStart + 1, wordPos = minWordMatchPos;
|
||||
wordPos < nextMaxWordMatchPos;
|
||||
column++, wordPos++
|
||||
) {
|
||||
let score = Number.MIN_SAFE_INTEGER;
|
||||
let canComeDiag = false;
|
||||
|
||||
if (wordPos <= maxWordMatchPos) {
|
||||
score = _doScore(
|
||||
pattern,
|
||||
patternLow,
|
||||
patternPos,
|
||||
patternStart,
|
||||
word,
|
||||
wordLow,
|
||||
wordPos,
|
||||
wordLen,
|
||||
wordStart,
|
||||
_diag[row - 1][column - 1] === 0,
|
||||
hasStrongFirstMatch
|
||||
);
|
||||
}
|
||||
|
||||
let diagScore = 0;
|
||||
if (score !== Number.MAX_SAFE_INTEGER) {
|
||||
canComeDiag = true;
|
||||
diagScore = score + _table[row - 1][column - 1];
|
||||
}
|
||||
|
||||
const canComeLeft = wordPos > minWordMatchPos;
|
||||
const leftScore = canComeLeft
|
||||
? _table[row][column - 1] + (_diag[row][column - 1] > 0 ? -5 : 0)
|
||||
: 0; // penalty for a gap start
|
||||
|
||||
const canComeLeftLeft =
|
||||
wordPos > minWordMatchPos + 1 && _diag[row][column - 1] > 0;
|
||||
const leftLeftScore = canComeLeftLeft
|
||||
? _table[row][column - 2] + (_diag[row][column - 2] > 0 ? -5 : 0)
|
||||
: 0; // penalty for a gap start
|
||||
|
||||
if (
|
||||
canComeLeftLeft &&
|
||||
(!canComeLeft || leftLeftScore >= leftScore) &&
|
||||
(!canComeDiag || leftLeftScore >= diagScore)
|
||||
) {
|
||||
// always prefer choosing left left to jump over a diagonal because that means a match is earlier in the word
|
||||
_table[row][column] = leftLeftScore;
|
||||
_arrows[row][column] = Arrow.LeftLeft;
|
||||
_diag[row][column] = 0;
|
||||
} else if (canComeLeft && (!canComeDiag || leftScore >= diagScore)) {
|
||||
// always prefer choosing left since that means a match is earlier in the word
|
||||
_table[row][column] = leftScore;
|
||||
_arrows[row][column] = Arrow.Left;
|
||||
_diag[row][column] = 0;
|
||||
} else if (canComeDiag) {
|
||||
_table[row][column] = diagScore;
|
||||
_arrows[row][column] = Arrow.Diag;
|
||||
_diag[row][column] = _diag[row - 1][column - 1] + 1;
|
||||
} else {
|
||||
throw new Error(`not possible`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_debug) {
|
||||
printTables(pattern, patternStart, word, wordStart);
|
||||
}
|
||||
|
||||
if (!hasStrongFirstMatch[0] && !firstMatchCanBeWeak) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
row--;
|
||||
column--;
|
||||
|
||||
const result: FuzzyScore = [_table[row][column], wordStart];
|
||||
|
||||
let backwardsDiagLength = 0;
|
||||
let maxMatchColumn = 0;
|
||||
|
||||
while (row >= 1) {
|
||||
// Find the column where we go diagonally up
|
||||
let diagColumn = column;
|
||||
do {
|
||||
const arrow = _arrows[row][diagColumn];
|
||||
if (arrow === Arrow.LeftLeft) {
|
||||
diagColumn -= 2;
|
||||
} else if (arrow === Arrow.Left) {
|
||||
diagColumn -= 1;
|
||||
} else {
|
||||
// found the diagonal
|
||||
break;
|
||||
}
|
||||
} while (diagColumn >= 1);
|
||||
|
||||
// Overturn the "forwards" decision if keeping the "backwards" diagonal would give a better match
|
||||
if (
|
||||
backwardsDiagLength > 1 && // only if we would have a contiguous match of 3 characters
|
||||
patternLow[patternStart + row - 1] === wordLow[wordStart + column - 1] && // only if we can do a contiguous match diagonally
|
||||
!isUpperCaseAtPos(diagColumn + wordStart - 1, word, wordLow) && // only if the forwards chose diagonal is not an uppercase
|
||||
backwardsDiagLength + 1 > _diag[row][diagColumn] // only if our contiguous match would be longer than the "forwards" contiguous match
|
||||
) {
|
||||
diagColumn = column;
|
||||
}
|
||||
|
||||
if (diagColumn === column) {
|
||||
// this is a contiguous match
|
||||
backwardsDiagLength++;
|
||||
} else {
|
||||
backwardsDiagLength = 1;
|
||||
}
|
||||
|
||||
if (!maxMatchColumn) {
|
||||
// remember the last matched column
|
||||
maxMatchColumn = diagColumn;
|
||||
}
|
||||
|
||||
row--;
|
||||
column = diagColumn - 1;
|
||||
result.push(column);
|
||||
}
|
||||
|
||||
if (wordLen === patternLen) {
|
||||
// the word matches the pattern with all characters!
|
||||
// giving the score a total match boost (to come up ahead other words)
|
||||
result[0] += 2;
|
||||
}
|
||||
|
||||
// Add 1 penalty for each skipped character in the word
|
||||
const skippedCharsCount = maxMatchColumn - patternLen;
|
||||
result[0] -= skippedCharsCount;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function _doScore(
|
||||
pattern: string,
|
||||
patternLow: string,
|
||||
patternPos: number,
|
||||
patternStart: number,
|
||||
word: string,
|
||||
wordLow: string,
|
||||
wordPos: number,
|
||||
wordLen: number,
|
||||
wordStart: number,
|
||||
newMatchStart: boolean,
|
||||
outFirstMatchStrong: boolean[]
|
||||
): number {
|
||||
if (patternLow[patternPos] !== wordLow[wordPos]) {
|
||||
return Number.MIN_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
let score = 1;
|
||||
let isGapLocation = false;
|
||||
if (wordPos === patternPos - patternStart) {
|
||||
// common prefix: `foobar <-> foobaz`
|
||||
// ^^^^^
|
||||
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
|
||||
} else if (
|
||||
isUpperCaseAtPos(wordPos, word, wordLow) &&
|
||||
(wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))
|
||||
) {
|
||||
// hitting upper-case: `foo <-> forOthers`
|
||||
// ^^ ^
|
||||
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
|
||||
isGapLocation = true;
|
||||
} else if (
|
||||
isSeparatorAtPos(wordLow, wordPos) &&
|
||||
(wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))
|
||||
) {
|
||||
// hitting a separator: `. <-> foo.bar`
|
||||
// ^
|
||||
score = 5;
|
||||
} else if (
|
||||
isSeparatorAtPos(wordLow, wordPos - 1) ||
|
||||
isWhitespaceAtPos(wordLow, wordPos - 1)
|
||||
) {
|
||||
// post separator: `foo <-> bar_foo`
|
||||
// ^^^
|
||||
score = 5;
|
||||
isGapLocation = true;
|
||||
}
|
||||
|
||||
if (score > 1 && patternPos === patternStart) {
|
||||
outFirstMatchStrong[0] = true;
|
||||
}
|
||||
|
||||
if (!isGapLocation) {
|
||||
isGapLocation =
|
||||
isUpperCaseAtPos(wordPos, word, wordLow) ||
|
||||
isSeparatorAtPos(wordLow, wordPos - 1) ||
|
||||
isWhitespaceAtPos(wordLow, wordPos - 1);
|
||||
}
|
||||
|
||||
//
|
||||
if (patternPos === patternStart) {
|
||||
// first character in pattern
|
||||
if (wordPos > wordStart) {
|
||||
// the first pattern character would match a word character that is not at the word start
|
||||
// so introduce a penalty to account for the gap preceding this match
|
||||
score -= isGapLocation ? 3 : 5;
|
||||
}
|
||||
} else if (newMatchStart) {
|
||||
// this would be the beginning of a new match (i.e. there would be a gap before this location)
|
||||
score += isGapLocation ? 2 : 0;
|
||||
} else {
|
||||
// this is part of a contiguous match, so give it a slight bonus, but do so only if it would not be a prefered gap location
|
||||
score += isGapLocation ? 0 : 1;
|
||||
}
|
||||
|
||||
if (wordPos + 1 === wordLen) {
|
||||
// we always penalize gaps, but this gives unfair advantages to a match that would match the last character in the word
|
||||
// so pretend there is a gap after the last character in the word to normalize things
|
||||
score -= isGapLocation ? 3 : 5;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
function printTable(
|
||||
table: number[][],
|
||||
pattern: string,
|
||||
patternLen: number,
|
||||
word: string,
|
||||
wordLen: number
|
||||
): string {
|
||||
function pad(s: string, n: number, _pad = " ") {
|
||||
while (s.length < n) {
|
||||
s = _pad + s;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
let ret = ` | |${word
|
||||
.split("")
|
||||
.map((c) => pad(c, 3))
|
||||
.join("|")}\n`;
|
||||
|
||||
for (let i = 0; i <= patternLen; i++) {
|
||||
if (i === 0) {
|
||||
ret += " |";
|
||||
} else {
|
||||
ret += `${pattern[i - 1]}|`;
|
||||
}
|
||||
ret +=
|
||||
table[i]
|
||||
.slice(0, wordLen + 1)
|
||||
.map((n) => pad(n.toString(), 3))
|
||||
.join("|") + "\n";
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function printTables(
|
||||
pattern: string,
|
||||
patternStart: number,
|
||||
word: string,
|
||||
wordStart: number
|
||||
): void {
|
||||
pattern = pattern.substr(patternStart);
|
||||
word = word.substr(wordStart);
|
||||
console.log(printTable(_table, pattern, pattern.length, word, word.length));
|
||||
console.log(printTable(_arrows, pattern, pattern.length, word, word.length));
|
||||
console.log(printTable(_diag, pattern, pattern.length, word, word.length));
|
||||
}
|
||||
|
||||
const _minWordMatchPos = initArr(2 * _maxLen); // min word position for a certain pattern position
|
||||
const _maxWordMatchPos = initArr(2 * _maxLen); // max word position for a certain pattern position
|
||||
const _diag = initTable(); // the length of a contiguous diagonal match
|
||||
const _table = initTable();
|
||||
const _arrows = <Arrow[][]>initTable();
|
||||
|
||||
function initArr(maxLen: number) {
|
||||
const row: number[] = [];
|
||||
for (let i = 0; i <= maxLen; i++) {
|
||||
row[i] = 0;
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
function _fillInMaxWordMatchPos(
|
||||
patternLen: number,
|
||||
wordLen: number,
|
||||
patternStart: number,
|
||||
wordStart: number,
|
||||
patternLow: string,
|
||||
wordLow: string
|
||||
) {
|
||||
let patternPos = patternLen - 1;
|
||||
let wordPos = wordLen - 1;
|
||||
while (patternPos >= patternStart && wordPos >= wordStart) {
|
||||
if (patternLow[patternPos] === wordLow[wordPos]) {
|
||||
_maxWordMatchPos[patternPos] = wordPos;
|
||||
patternPos--;
|
||||
}
|
||||
wordPos--;
|
||||
}
|
||||
}
|
||||
|
||||
export interface FuzzyScorer {
|
||||
(
|
||||
pattern: string,
|
||||
lowPattern: string,
|
||||
patternPos: number,
|
||||
word: string,
|
||||
lowWord: string,
|
||||
wordPos: number,
|
||||
firstMatchCanBeWeak: boolean
|
||||
): FuzzyScore | undefined;
|
||||
}
|
||||
|
||||
export function createMatches(score: undefined | FuzzyScore): Match[] {
|
||||
if (typeof score === "undefined") {
|
||||
return [];
|
||||
}
|
||||
const res: Match[] = [];
|
||||
const wordPos = score[1];
|
||||
for (let i = score.length - 1; i > 1; i--) {
|
||||
const pos = score[i] + wordPos;
|
||||
const last = res[res.length - 1];
|
||||
if (last && last.end === pos) {
|
||||
last.end = pos + 1;
|
||||
} else {
|
||||
res.push({ start: pos, end: pos + 1 });
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* A fast function (therefore imprecise) to check if code points are emojis.
|
||||
* Generated using https://github.com/alexdima/unicode-utils/blob/master/generate-emoji-test.js
|
||||
*/
|
||||
export function isEmojiImprecise(x: number): boolean {
|
||||
return (
|
||||
(x >= 0x1f1e6 && x <= 0x1f1ff) ||
|
||||
x === 8986 ||
|
||||
x === 8987 ||
|
||||
x === 9200 ||
|
||||
x === 9203 ||
|
||||
(x >= 9728 && x <= 10175) ||
|
||||
x === 11088 ||
|
||||
x === 11093 ||
|
||||
(x >= 127744 && x <= 128591) ||
|
||||
(x >= 128640 && x <= 128764) ||
|
||||
(x >= 128992 && x <= 129003) ||
|
||||
(x >= 129280 && x <= 129535) ||
|
||||
(x >= 129648 && x <= 129750)
|
||||
);
|
||||
}
|
@@ -1,52 +1,4 @@
|
||||
import { fuzzyScore } from "./filter";
|
||||
|
||||
/**
|
||||
* Determine whether a sequence of letters exists in another string,
|
||||
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
|
||||
*
|
||||
* @param {string} filter - Sequence of letters to check for
|
||||
* @param {ScorableTextItem} item - Item against whose strings will be checked
|
||||
*
|
||||
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
|
||||
*/
|
||||
|
||||
export const fuzzySequentialMatch = (
|
||||
filter: string,
|
||||
item: ScorableTextItem
|
||||
) => {
|
||||
let topScore = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (const word of item.strings) {
|
||||
const scores = fuzzyScore(
|
||||
filter,
|
||||
filter.toLowerCase(),
|
||||
0,
|
||||
word,
|
||||
word.toLowerCase(),
|
||||
0,
|
||||
true
|
||||
);
|
||||
|
||||
if (!scores) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The VS Code implementation of filter returns a 0 for a weak match.
|
||||
// But if .filter() sees a "0", it considers that a failed match and will remove it.
|
||||
// So, we set score to 1 in these cases so the match will be included, and mostly respect correct ordering.
|
||||
const score = scores[0] === 0 ? 1 : scores[0];
|
||||
|
||||
if (score > topScore) {
|
||||
topScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
if (topScore === Number.NEGATIVE_INFINITY) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return topScore;
|
||||
};
|
||||
import fuzzysort from "fuzzysort";
|
||||
|
||||
/**
|
||||
* An interface that objects must extend in order to use the fuzzy sequence matcher
|
||||
@@ -66,18 +18,48 @@ export interface ScorableTextItem {
|
||||
strings: string[];
|
||||
}
|
||||
|
||||
type FuzzyFilterSort = <T extends ScorableTextItem>(
|
||||
export type FuzzyFilterSort = <T extends ScorableTextItem>(
|
||||
filter: string,
|
||||
items: T[]
|
||||
) => T[];
|
||||
|
||||
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) =>
|
||||
items
|
||||
export function fuzzyMatcher(search: string | null): (string) => boolean {
|
||||
const scorer = fuzzyScorer(search);
|
||||
return (value: string) => scorer([value]) !== Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
|
||||
export function fuzzyScorer(
|
||||
search: string | null
|
||||
): (values: string[]) => number {
|
||||
const searchTerms = (search || "").match(/("[^"]+"|[^"\s]+)/g);
|
||||
if (!searchTerms) {
|
||||
return () => 0;
|
||||
}
|
||||
return (values) =>
|
||||
searchTerms
|
||||
.map((term) => {
|
||||
const resultsForTerm = fuzzysort.go(term, values, {
|
||||
allowTypo: true,
|
||||
});
|
||||
if (resultsForTerm.length > 0) {
|
||||
return Math.max(...resultsForTerm.map((result) => result.score));
|
||||
}
|
||||
return Number.NEGATIVE_INFINITY;
|
||||
})
|
||||
.reduce((partial, current) => partial + current, 0);
|
||||
}
|
||||
|
||||
export const fuzzySortFilterSort: FuzzyFilterSort = (filter, items) => {
|
||||
const scorer = fuzzyScorer(filter);
|
||||
return items
|
||||
.map((item) => {
|
||||
item.score = fuzzySequentialMatch(filter, item);
|
||||
item.score = scorer(item.strings);
|
||||
return item;
|
||||
})
|
||||
.filter((item) => item.score !== undefined)
|
||||
.filter((item) => item.score !== undefined && item.score > -100000)
|
||||
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
|
||||
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
|
||||
);
|
||||
};
|
||||
|
||||
export const defaultFuzzyFilterSort = fuzzySortFilterSort;
|
||||
|
@@ -7,25 +7,26 @@ import type {
|
||||
SortableColumnContainer,
|
||||
SortingDirection,
|
||||
} from "./ha-data-table";
|
||||
import { fuzzyMatcher } from "../../common/string/filter/sequence-matching";
|
||||
|
||||
const filterData = (
|
||||
data: DataTableRowData[],
|
||||
columns: SortableColumnContainer,
|
||||
filter: string
|
||||
) => {
|
||||
filter = filter.toUpperCase();
|
||||
const matcher = fuzzyMatcher(filter);
|
||||
return data.filter((row) =>
|
||||
Object.entries(columns).some((columnEntry) => {
|
||||
const [key, column] = columnEntry;
|
||||
if (column.filterable) {
|
||||
if (
|
||||
String(
|
||||
column.filterKey
|
||||
? row[column.valueColumn || key][column.filterKey]
|
||||
: row[column.valueColumn || key]
|
||||
matcher(
|
||||
String(
|
||||
column.filterKey
|
||||
? row[column.valueColumn || key][column.filterKey]
|
||||
: row[column.valueColumn || key]
|
||||
)
|
||||
)
|
||||
.toUpperCase()
|
||||
.includes(filter)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
@@ -198,9 +198,10 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
this.hass,
|
||||
deviceEntityLookup[device.id]
|
||||
),
|
||||
area: device.area_id
|
||||
? areaLookup[device.area_id].name
|
||||
: this.hass.localize("ui.components.device-picker.no_area"),
|
||||
area:
|
||||
device.area_id && areaLookup[device.area_id]
|
||||
? areaLookup[device.area_id].name
|
||||
: this.hass.localize("ui.components.device-picker.no_area"),
|
||||
}));
|
||||
if (!outputDevices.length) {
|
||||
return [
|
||||
|
@@ -15,6 +15,7 @@ import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
import { defaultFuzzyFilterSort } from "../../common/string/filter/sequence-matching";
|
||||
|
||||
interface HassEntityWithCachedName extends HassEntity {
|
||||
friendly_name: string;
|
||||
@@ -336,11 +337,18 @@ export class HaEntityPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
const filterString = ev.detail.value.toLowerCase();
|
||||
(this.comboBox as any).filteredItems = this._states.filter(
|
||||
(entityState) =>
|
||||
entityState.entity_id.toLowerCase().includes(filterString) ||
|
||||
computeStateName(entityState).toLowerCase().includes(filterString)
|
||||
const filterString = ev.detail.value;
|
||||
|
||||
const sortableEntityStates = this._states.map((entityState) => ({
|
||||
strings: [entityState.entity_id, computeStateName(entityState)],
|
||||
entityState: entityState,
|
||||
}));
|
||||
const sortedEntityStates = defaultFuzzyFilterSort(
|
||||
filterString,
|
||||
sortableEntityStates
|
||||
);
|
||||
(this.comboBox as any).filteredItems = sortedEntityStates.map(
|
||||
(sortableItem) => sortableItem.entityState
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -10,8 +10,8 @@ interface State {
|
||||
backgroundColor: null | string;
|
||||
}
|
||||
|
||||
@customElement("hassio-ansi-to-html")
|
||||
class HassioAnsiToHtml extends LitElement {
|
||||
@customElement("ha-ansi-to-html")
|
||||
class HaAnsiToHtml extends LitElement {
|
||||
@property() public content!: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
@@ -241,6 +241,6 @@ class HassioAnsiToHtml extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-ansi-to-html": HassioAnsiToHtml;
|
||||
"ha-ansi-to-html": HaAnsiToHtml;
|
||||
}
|
||||
}
|
@@ -409,7 +409,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
name,
|
||||
});
|
||||
this._areas = [...this._areas!, area];
|
||||
(this.comboBox as any).items = this._getAreas(
|
||||
(this.comboBox as any).filteredItems = this._getAreas(
|
||||
this._areas!,
|
||||
this._devices!,
|
||||
this._entities!,
|
||||
|
@@ -56,6 +56,9 @@ class HaNavigationList extends LitElement {
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
--mdc-list-vertical-padding: 0;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
@@ -68,6 +71,7 @@ class HaNavigationList extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: block;
|
||||
}
|
||||
ha-svg-icon {
|
||||
padding: 8px;
|
||||
|
@@ -163,6 +163,9 @@ export class HaNetwork extends LitElement {
|
||||
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
--paper-time-input-justify-content: flex-end;
|
||||
--settings-row-content-display: contents;
|
||||
--settings-row-prefix-display: contents;
|
||||
}
|
||||
|
||||
span[slot="heading"],
|
||||
|
@@ -472,6 +472,7 @@ export class HaServiceControl extends LitElement {
|
||||
ha-settings-row {
|
||||
--paper-time-input-justify-content: flex-end;
|
||||
--settings-row-content-width: 100%;
|
||||
--settings-row-prefix-display: contents;
|
||||
border-top: var(
|
||||
--service-control-items-border-top,
|
||||
1px solid var(--divider-color)
|
||||
|
@@ -47,7 +47,7 @@ export class HaSettingsRow extends LitElement {
|
||||
display: contents;
|
||||
}
|
||||
:host(:not([narrow])) .content {
|
||||
display: flex;
|
||||
display: var(--settings-row-content-display, flex);
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
padding: 16px 0;
|
||||
@@ -68,7 +68,7 @@ export class HaSettingsRow extends LitElement {
|
||||
white-space: normal;
|
||||
}
|
||||
.prefix-wrap {
|
||||
display: contents;
|
||||
display: var(--settings-row-prefix-display);
|
||||
}
|
||||
:host([narrow]) .prefix-wrap {
|
||||
display: flex;
|
||||
|
@@ -1051,9 +1051,6 @@ class HaSidebar extends LitElement {
|
||||
padding: 0px 6px;
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
}
|
||||
.configuration-badge {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
ha-svg-icon + .notification-badge,
|
||||
ha-svg-icon + .configuration-badge {
|
||||
position: absolute;
|
||||
|
@@ -1,10 +1,15 @@
|
||||
import type {
|
||||
HassEntities,
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { BINARY_STATE_ON } from "../common/const";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { showToast } from "../util/toast";
|
||||
|
||||
export const UPDATE_SUPPORT_INSTALL = 1;
|
||||
export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2;
|
||||
@@ -47,3 +52,75 @@ export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
|
||||
type: "update/release_notes",
|
||||
entity_id: entityId,
|
||||
});
|
||||
|
||||
export const filterUpdateEntities = (entities: HassEntities) =>
|
||||
(
|
||||
Object.values(entities).filter(
|
||||
(entity) => computeStateDomain(entity) === "update"
|
||||
) as UpdateEntity[]
|
||||
).sort((a, b) => {
|
||||
if (a.attributes.title === "Home Assistant Core") {
|
||||
return -3;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Core") {
|
||||
return 3;
|
||||
}
|
||||
if (a.attributes.title === "Home Assistant Operating System") {
|
||||
return -2;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Operating System") {
|
||||
return 2;
|
||||
}
|
||||
if (a.attributes.title === "Home Assistant Supervisor") {
|
||||
return -1;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Supervisor") {
|
||||
return 1;
|
||||
}
|
||||
return caseInsensitiveStringCompare(
|
||||
a.attributes.title || a.attributes.friendly_name || "",
|
||||
b.attributes.title || b.attributes.friendly_name || ""
|
||||
);
|
||||
});
|
||||
|
||||
export const filterUpdateEntitiesWithInstall = (
|
||||
entities: HassEntities,
|
||||
showSkipped = false
|
||||
) =>
|
||||
filterUpdateEntities(entities).filter((entity) =>
|
||||
updateCanInstall(entity, showSkipped)
|
||||
);
|
||||
|
||||
export const checkForEntityUpdates = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant
|
||||
) => {
|
||||
const entities = filterUpdateEntities(hass.states).map(
|
||||
(entity) => entity.entity_id
|
||||
);
|
||||
|
||||
if (!entities.length) {
|
||||
showAlertDialog(element, {
|
||||
title: hass.localize("ui.panel.config.updates.no_update_entities.title"),
|
||||
text: hass.localize(
|
||||
"ui.panel.config.updates.no_update_entities.description"
|
||||
),
|
||||
warning: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await hass.callService("homeassistant", "update_entity", {
|
||||
entity_id: entities,
|
||||
});
|
||||
|
||||
if (filterUpdateEntitiesWithInstall(hass.states).length) {
|
||||
showToast(element, {
|
||||
message: hass.localize("ui.panel.config.updates.updates_refreshed"),
|
||||
});
|
||||
} else {
|
||||
showToast(element, {
|
||||
message: hass.localize("ui.panel.config.updates.no_new_updates"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -24,7 +24,7 @@ import { domainIcon } from "../../common/entity/domain_icon";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import {
|
||||
fuzzyFilterSort,
|
||||
defaultFuzzyFilterSort,
|
||||
ScorableTextItem,
|
||||
} from "../../common/string/filter/sequence-matching";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
@@ -694,7 +694,7 @@ export class QuickBar extends LitElement {
|
||||
|
||||
private _filterItems = memoizeOne(
|
||||
(items: QuickBarItem[], filter: string): QuickBarItem[] =>
|
||||
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
|
||||
defaultFuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
|
||||
);
|
||||
|
||||
static get styles() {
|
||||
|
@@ -99,6 +99,7 @@ class HassSubpage extends LitElement {
|
||||
ha-icon-button-arrow-prev,
|
||||
::slotted([slot="toolbar-icon"]) {
|
||||
pointer-events: auto;
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
.main-title {
|
||||
|
@@ -283,6 +283,9 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
height: calc(100vh - 1px - var(--header-height));
|
||||
display: block;
|
||||
}
|
||||
:host([narrow]) hass-tabs-subpage {
|
||||
--main-title-margin: 0;
|
||||
}
|
||||
.table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@@ -82,6 +82,16 @@ class HassTabsSubpage extends LitElement {
|
||||
(!page.advancedOnly || showAdvanced)
|
||||
);
|
||||
|
||||
if (shownTabs.length < 2) {
|
||||
if (shownTabs.length === 1) {
|
||||
const page = shownTabs[0];
|
||||
return [
|
||||
page.translationKey ? localizeFunc(page.translationKey) : page.name,
|
||||
];
|
||||
}
|
||||
return [""];
|
||||
}
|
||||
|
||||
return shownTabs.map(
|
||||
(page) =>
|
||||
html`
|
||||
@@ -134,7 +144,7 @@ class HassTabsSubpage extends LitElement {
|
||||
this.narrow,
|
||||
this.localizeFunc || this.hass.localize
|
||||
);
|
||||
const showTabs = tabs.length > 1 || !this.narrow;
|
||||
const showTabs = tabs.length > 1;
|
||||
return html`
|
||||
<div class="toolbar">
|
||||
${this.mainPage || (!this.backPath && history.state?.root)
|
||||
@@ -159,8 +169,10 @@ class HassTabsSubpage extends LitElement {
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`}
|
||||
${this.narrow
|
||||
? html`<div class="main-title"><slot name="header"></slot></div>`
|
||||
${this.narrow || !showTabs
|
||||
? html`<div class="main-title">
|
||||
<slot name="header">${!showTabs ? tabs[0] : ""}</slot>
|
||||
</div>`
|
||||
: ""}
|
||||
${showTabs
|
||||
? html`
|
||||
@@ -283,6 +295,7 @@ class HassTabsSubpage extends LitElement {
|
||||
max-height: var(--header-height);
|
||||
line-height: 20px;
|
||||
color: var(--sidebar-text-color);
|
||||
margin: var(--main-title-margin, 0 0 0 24px);
|
||||
}
|
||||
|
||||
.content {
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../hassio/src/components/hassio-ansi-to-html";
|
||||
import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload";
|
||||
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-card";
|
||||
import "../components/ha-ansi-to-html";
|
||||
import { fetchInstallationType } from "../data/onboarding";
|
||||
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
||||
import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
|
||||
@@ -86,7 +86,7 @@ class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) {
|
||||
padding: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
hassio-ansi-to-html {
|
||||
ha-ansi-to-html {
|
||||
display: block;
|
||||
line-height: 22px;
|
||||
padding: 0 8px;
|
||||
|
@@ -332,6 +332,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
ha-settings-row {
|
||||
--paper-time-input-justify-content: flex-end;
|
||||
--settings-row-content-width: 100%;
|
||||
--settings-row-prefix-display: contents;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
`,
|
||||
|
@@ -80,6 +80,7 @@ class HaConfigBackup extends LitElement {
|
||||
actions: {
|
||||
title: "",
|
||||
width: "15%",
|
||||
type: "overflow-menu",
|
||||
template: (_: string, backup: BackupContent) =>
|
||||
html`<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
@@ -126,17 +127,23 @@ class HaConfigBackup extends LitElement {
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.tabs=${[
|
||||
{
|
||||
translationKey: "ui.panel.config.backup.caption",
|
||||
path: `/config/backup`,
|
||||
},
|
||||
]}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/system"
|
||||
.route=${this.route}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._getItems(this._backupData.backups)}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.backup.no_bakcups")}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.backup.no_backups")}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.backup.picker.search"
|
||||
)}
|
||||
>
|
||||
<span slot="header"
|
||||
>${this.hass.localize("ui.panel.config.backup.caption")}</span
|
||||
>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
?disabled=${this._backupData.backing_up}
|
||||
|
@@ -58,9 +58,9 @@ class ConfigAnalytics extends LitElement {
|
||||
"ui.panel.config.core.section.core.core_config.save_button"
|
||||
)}
|
||||
</mwc-button>
|
||||
${analyticsLearnMore(this.hass)}
|
||||
</div>
|
||||
</ha-card>
|
||||
<div class="footer">${analyticsLearnMore(this.hass)}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -117,6 +117,10 @@ class ConfigAnalytics extends LitElement {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.footer {
|
||||
padding: 32px 0 16px;
|
||||
text-align: center;
|
||||
}
|
||||
`, // row-reverse so we tab first to "save"
|
||||
];
|
||||
}
|
||||
|
361
src/panels/config/core/ha-config-section-general.ts
Normal file
361
src/panels/config/core/ha-config-section-general.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import timezones from "google-timezones-json";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { UNIT_C } from "../../../common/const";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import "../../../components/buttons/ha-progress-button";
|
||||
import type { HaProgressButton } from "../../../components/buttons/ha-progress-button";
|
||||
import { currencies } from "../../../components/currency-datalist";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../components/ha-radio";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/map/ha-locations-editor";
|
||||
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||
import { SYMBOL_TO_ISO } from "../../../data/currency";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("ha-config-section-general")
|
||||
class HaConfigSectionGeneral extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
|
||||
|
||||
@state() private _currency?: string;
|
||||
|
||||
@state() private _name?: string;
|
||||
|
||||
@state() private _elevation?: number;
|
||||
|
||||
@state() private _timeZone?: string;
|
||||
|
||||
@state() private _location?: [number, number];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canEdit = ["storage", "default"].includes(
|
||||
this.hass.config.config_source
|
||||
);
|
||||
const disabled = this._submitting || !canEdit;
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.core.caption")}
|
||||
>
|
||||
<div class="content">
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
${!canEdit
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
name="name"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.location_name"
|
||||
)}
|
||||
.disabled=${disabled}
|
||||
.value=${this._name}
|
||||
@change=${this._handleChange}
|
||||
></ha-textfield>
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.time_zone"
|
||||
)}
|
||||
name="timeZone"
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.disabled=${disabled}
|
||||
.value=${this._timeZone}
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
${Object.keys(timezones).map(
|
||||
(tz) =>
|
||||
html`<mwc-list-item value=${tz}
|
||||
>${timezones[tz]}</mwc-list-item
|
||||
>`
|
||||
)}
|
||||
</ha-select>
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation"
|
||||
)}
|
||||
name="elevation"
|
||||
type="number"
|
||||
.disabled=${disabled}
|
||||
.value=${this._elevation}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
<span slot="suffix">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation_meters"
|
||||
)}
|
||||
</span>
|
||||
</ha-textfield>
|
||||
<div>
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system"
|
||||
)}
|
||||
</div>
|
||||
<ha-formfield
|
||||
.label=${html`
|
||||
<span style="font-size: 14px">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.metric_example"
|
||||
)}
|
||||
</span>
|
||||
<div style="color: var(--secondary-text-color)">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_metric"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="metric"
|
||||
.checked=${this._unitSystem === "metric"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${this._submitting}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`
|
||||
<span style="font-size: 14px">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.imperial_example"
|
||||
)}
|
||||
</span>
|
||||
<div style="color: var(--secondary-text-color)">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_imperial"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="imperial"
|
||||
.checked=${this._unitSystem === "imperial"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${this._submitting}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<div>
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.currency"
|
||||
)}
|
||||
name="currency"
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.disabled=${disabled}
|
||||
.value=${this._currency}
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
${currencies.map(
|
||||
(currency) =>
|
||||
html`<mwc-list-item .value=${currency}
|
||||
>${currency}</mwc-list-item
|
||||
>`
|
||||
)}</ha-select
|
||||
>
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.find_currency_value"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
${this.narrow
|
||||
? html`
|
||||
<ha-locations-editor
|
||||
.hass=${this.hass}
|
||||
.locations=${this._markerLocation(
|
||||
this.hass.config.latitude,
|
||||
this.hass.config.longitude,
|
||||
this._location
|
||||
)}
|
||||
@location-updated=${this._locationChanged}
|
||||
></ha-locations-editor>
|
||||
`
|
||||
: html`
|
||||
<ha-settings-row>
|
||||
<div slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_location"
|
||||
)}
|
||||
</div>
|
||||
<div slot="description" class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_location_description"
|
||||
)}
|
||||
</div>
|
||||
<mwc-button @click=${this._editLocation}
|
||||
>${this.hass.localize("ui.common.edit")}</mwc-button
|
||||
>
|
||||
</ha-settings-row>
|
||||
`}
|
||||
<div class="card-actions">
|
||||
<ha-progress-button @click=${this._updateEntry}>
|
||||
${this.hass!.localize("ui.panel.config.zone.detail.update")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._unitSystem =
|
||||
this.hass.config.unit_system.temperature === UNIT_C
|
||||
? "metric"
|
||||
: "imperial";
|
||||
this._currency = this.hass.config.currency;
|
||||
this._elevation = this.hass.config.elevation;
|
||||
this._timeZone = this.hass.config.time_zone;
|
||||
this._name = this.hass.config.location_name;
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
const target = ev.currentTarget;
|
||||
let value = target.value;
|
||||
|
||||
if (target.name === "currency" && value) {
|
||||
if (value in SYMBOL_TO_ISO) {
|
||||
value = SYMBOL_TO_ISO[value];
|
||||
}
|
||||
}
|
||||
|
||||
this[`_${target.name}`] = value;
|
||||
}
|
||||
|
||||
private _unitSystemChanged(ev: CustomEvent) {
|
||||
this._unitSystem = (ev.target as HaRadio).value as "metric" | "imperial";
|
||||
}
|
||||
|
||||
private _locationChanged(ev: CustomEvent) {
|
||||
this._location = ev.detail.location;
|
||||
}
|
||||
|
||||
private async _updateEntry(ev: CustomEvent) {
|
||||
const button = ev.target as HaProgressButton;
|
||||
if (button.progress) {
|
||||
return;
|
||||
}
|
||||
button.progress = true;
|
||||
|
||||
try {
|
||||
await saveCoreConfig(this.hass, {
|
||||
currency: this._currency,
|
||||
elevation: Number(this._elevation),
|
||||
unit_system: this._unitSystem,
|
||||
time_zone: this._timeZone,
|
||||
location_name: this._name,
|
||||
});
|
||||
button.actionSuccess();
|
||||
} catch (err: any) {
|
||||
button.actionError();
|
||||
alert(`Error saving config: ${err.message}`);
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _markerLocation = memoizeOne(
|
||||
(
|
||||
lat: number,
|
||||
lng: number,
|
||||
location?: [number, number]
|
||||
): MarkerLocation[] => [
|
||||
{
|
||||
id: "location",
|
||||
latitude: location ? location[0] : lat,
|
||||
longitude: location ? location[1] : lng,
|
||||
location_editable: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
private _editLocation() {
|
||||
navigate("/config/zone");
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.card-content > * {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-select {
|
||||
display: block;
|
||||
}
|
||||
ha-locations-editor {
|
||||
display: block;
|
||||
height: 400px;
|
||||
padding: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-section-general": HaConfigSectionGeneral;
|
||||
}
|
||||
}
|
@@ -1,22 +1,35 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-bar";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-metric";
|
||||
import { updateCanInstall, UpdateEntity } from "../../../data/update";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import {
|
||||
fetchHassioSupervisorInfo,
|
||||
HassioSupervisorInfo,
|
||||
reloadSupervisor,
|
||||
setSupervisorOption,
|
||||
SupervisorOptions,
|
||||
} from "../../../data/hassio/supervisor";
|
||||
import {
|
||||
checkForEntityUpdates,
|
||||
filterUpdateEntitiesWithInstall,
|
||||
} from "../../../data/update";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "../dashboard/ha-config-updates";
|
||||
import "./ha-config-analytics";
|
||||
|
||||
@customElement("ha-config-section-updates")
|
||||
class HaConfigSectionUpdates extends LitElement {
|
||||
@@ -26,7 +39,17 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
|
||||
@state() private _showSkipped = false;
|
||||
|
||||
private _notifyUpdates = false;
|
||||
@state() private _supervisorInfo?: HassioSupervisorInfo;
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
fetchHassioSupervisorInfo(this.hass).then((data) => {
|
||||
this._supervisorInfo = data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canInstallUpdates = this._filterUpdateEntitiesWithInstall(
|
||||
@@ -41,22 +64,38 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.updates.caption")}
|
||||
>
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
slot="toolbar-icon"
|
||||
@action=${this._toggleSkipped}
|
||||
>
|
||||
<div slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.panel.config.info.copy_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.updates.check_updates"
|
||||
)}
|
||||
.path=${mdiRefresh}
|
||||
@click=${this._checkUpdates}
|
||||
></ha-icon-button>
|
||||
<mwc-list-item>
|
||||
${this._showSkipped
|
||||
? this.hass.localize("ui.panel.config.updates.hide_skipped")
|
||||
: this.hass.localize("ui.panel.config.updates.show_skipped")}
|
||||
</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<mwc-list-item id="skipped">
|
||||
${this._showSkipped
|
||||
? this.hass.localize("ui.panel.config.updates.hide_skipped")
|
||||
: this.hass.localize("ui.panel.config.updates.show_skipped")}
|
||||
</mwc-list-item>
|
||||
${this._supervisorInfo?.channel !== "dev"
|
||||
? html`
|
||||
<mwc-list-item id="beta">
|
||||
${this._supervisorInfo?.channel === "stable"
|
||||
? this.hass.localize("ui.panel.config.updates.join_beta")
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.updates.leave_beta"
|
||||
)}
|
||||
</mwc-list-item>
|
||||
`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
<div class="content">
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
@@ -73,107 +112,68 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
${this.hass.localize("ui.panel.config.updates.no_updates")}
|
||||
`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._checkUpdates}>
|
||||
${this.hass.localize("ui.panel.config.updates.check_updates")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
protected override updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (
|
||||
!changedProps.has("hass") ||
|
||||
!this._notifyUpdates ||
|
||||
!changedProps.has("_showSkipped")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._notifyUpdates = false;
|
||||
if (
|
||||
this._filterUpdateEntitiesWithInstall(this.hass.states, this._showSkipped)
|
||||
.length
|
||||
) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.updates.updates_refreshed"
|
||||
),
|
||||
});
|
||||
} else {
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.panel.config.updates.no_new_updates"),
|
||||
});
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._showSkipped = !this._showSkipped;
|
||||
break;
|
||||
case 1:
|
||||
this._toggleBeta();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleSkipped(): void {
|
||||
this._showSkipped = !this._showSkipped;
|
||||
private async _toggleBeta(): Promise<void> {
|
||||
if (this._supervisorInfo!.channel === "stable") {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.dialogs.join_beta_channel.title"),
|
||||
text: html`${this.hass.localize("ui.dialogs.join_beta_channel.warning")}
|
||||
<br />
|
||||
<b> ${this.hass.localize("ui.dialogs.join_beta_channel.backup")} </b>
|
||||
<br /><br />
|
||||
${this.hass.localize("ui.dialogs.join_beta_channel.release_items")}
|
||||
<ul>
|
||||
<li>Home Assistant Core</li>
|
||||
<li>Home Assistant Supervisor</li>
|
||||
<li>Home Assistant Operating System</li>
|
||||
</ul>
|
||||
<br />
|
||||
${this.hass.localize("ui.dialogs.join_beta_channel.confirm")}`,
|
||||
confirmText: this.hass.localize("ui.panel.config.updates.join_beta"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const data: Partial<SupervisorOptions> = {
|
||||
channel: this._supervisorInfo!.channel === "stable" ? "beta" : "stable",
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
await reloadSupervisor(this.hass);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _checkUpdates(): Promise<void> {
|
||||
const _entities = this._filterUpdateEntities(this.hass.states).map(
|
||||
(entity) => entity.entity_id
|
||||
);
|
||||
|
||||
if (_entities.length) {
|
||||
this._notifyUpdates = true;
|
||||
await this.hass.callService("homeassistant", "update_entity", {
|
||||
entity_id: _entities,
|
||||
});
|
||||
return;
|
||||
}
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.updates.no_update_entities.title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.updates.no_update_entities.description"
|
||||
),
|
||||
warning: true,
|
||||
});
|
||||
checkForEntityUpdates(this, this.hass);
|
||||
}
|
||||
|
||||
private _filterUpdateEntities = memoizeOne((entities: HassEntities) =>
|
||||
(
|
||||
Object.values(entities).filter(
|
||||
(entity) => computeStateDomain(entity) === "update"
|
||||
) as UpdateEntity[]
|
||||
).sort((a, b) => {
|
||||
if (a.attributes.title === "Home Assistant Core") {
|
||||
return -3;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Core") {
|
||||
return 3;
|
||||
}
|
||||
if (a.attributes.title === "Home Assistant Operating System") {
|
||||
return -2;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Operating System") {
|
||||
return 2;
|
||||
}
|
||||
if (a.attributes.title === "Home Assistant Supervisor") {
|
||||
return -1;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Supervisor") {
|
||||
return 1;
|
||||
}
|
||||
return caseInsensitiveStringCompare(
|
||||
a.attributes.title || a.attributes.friendly_name || "",
|
||||
b.attributes.title || b.attributes.friendly_name || ""
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
private _filterUpdateEntitiesWithInstall = memoizeOne(
|
||||
(entities: HassEntities, showSkipped: boolean) =>
|
||||
this._filterUpdateEntities(entities).filter((entity) =>
|
||||
updateCanInstall(entity, showSkipped)
|
||||
)
|
||||
filterUpdateEntitiesWithInstall(entities, showSkipped)
|
||||
);
|
||||
|
||||
static styles = css`
|
||||
@@ -183,7 +183,7 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-card {
|
||||
max-width: 500px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
@@ -191,19 +191,12 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
display: flex;
|
||||
margin-bottom: max(24px, env(safe-area-inset-bottom));
|
||||
}
|
||||
.card-actions {
|
||||
height: 48px;
|
||||
border-top: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
padding: 16px 16px 0 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -1,9 +1,12 @@
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { css, CSSResultGroup, 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-navigation-list";
|
||||
import { CloudStatus } from "../../../data/cloud";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@@ -38,17 +41,28 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
back-path="/config"
|
||||
.header=${this.hass.localize("ui.panel.config.dashboard.system.main")}
|
||||
>
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
@action=${this._handleAction}
|
||||
slot="toolbar-icon"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<mwc-list-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.system_dashboard.restart_homeassistant"
|
||||
)}
|
||||
</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
<ha-config-section
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
full-width
|
||||
>
|
||||
<ha-card outlined>
|
||||
${this.narrow
|
||||
? html`<div class="title">
|
||||
${this.hass.localize("ui.panel.config.dashboard.system.main")}
|
||||
</div>`
|
||||
: ""}
|
||||
<ha-navigation-list
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
@@ -60,13 +74,25 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.system_dashboard.confirm_restart"
|
||||
),
|
||||
confirm: () => {
|
||||
this.hass.callService("homeassistant", "restart");
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -79,6 +105,8 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: max(24px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
ha-card a {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiCloudLock, mdiDotsVertical, mdiMagnify, mdiNewBox } from "@mdi/js";
|
||||
import { mdiCloudLock, mdiDotsVertical, mdiMagnify } from "@mdi/js";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -15,8 +15,6 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
@@ -25,15 +23,17 @@ import "../../../components/ha-menu-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tip";
|
||||
import { CloudStatus } from "../../../data/cloud";
|
||||
import { updateCanInstall, UpdateEntity } from "../../../data/update";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
checkForEntityUpdates,
|
||||
filterUpdateEntitiesWithInstall,
|
||||
UpdateEntity,
|
||||
} from "../../../data/update";
|
||||
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";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "./ha-config-navigation";
|
||||
@@ -81,12 +81,12 @@ const randomTip = (hass: HomeAssistant) => {
|
||||
rel="noreferrer"
|
||||
>Newsletter</a
|
||||
>
|
||||
<ha-svg-icon class="new" .path=${mdiNewBox}></ha-svg-icon
|
||||
></span>`
|
||||
</span>`
|
||||
),
|
||||
weight: 2,
|
||||
},
|
||||
{ content: hass.localize("ui.tips.key_c_hint"), weight: 1 },
|
||||
{ content: hass.localize("ui.tips.key_m_hint"), weight: 1 },
|
||||
];
|
||||
|
||||
tips.forEach((tip) => {
|
||||
@@ -113,8 +113,6 @@ class HaConfigDashboard extends LitElement {
|
||||
|
||||
@state() private _tip?: string;
|
||||
|
||||
private _notifyUpdates = false;
|
||||
|
||||
private _pages = memoizeOne((clouStatus, isLoaded) => {
|
||||
const pages: PageNavigation[] = [];
|
||||
if (clouStatus && isLoaded) {
|
||||
@@ -219,60 +217,12 @@ class HaConfigDashboard extends LitElement {
|
||||
if (!this._tip && changedProps.has("hass")) {
|
||||
this._tip = randomTip(this.hass);
|
||||
}
|
||||
|
||||
if (!changedProps.has("hass") || !this._notifyUpdates) {
|
||||
return;
|
||||
}
|
||||
this._notifyUpdates = false;
|
||||
if (this._filterUpdateEntitiesWithInstall(this.hass.states).length) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.updates.updates_refreshed"
|
||||
),
|
||||
});
|
||||
} else {
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.panel.config.updates.no_new_updates"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _filterUpdateEntities = memoizeOne((entities: HassEntities) =>
|
||||
(
|
||||
Object.values(entities).filter(
|
||||
(entity) => computeStateDomain(entity) === "update"
|
||||
) as UpdateEntity[]
|
||||
).sort((a, b) => {
|
||||
if (a.attributes.title === "Home Assistant Core") {
|
||||
return -3;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Core") {
|
||||
return 3;
|
||||
}
|
||||
if (a.attributes.title === "Home Assistant Operating System") {
|
||||
return -2;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Operating System") {
|
||||
return 2;
|
||||
}
|
||||
if (a.attributes.title === "Home Assistant Supervisor") {
|
||||
return -1;
|
||||
}
|
||||
if (b.attributes.title === "Home Assistant Supervisor") {
|
||||
return 1;
|
||||
}
|
||||
return caseInsensitiveStringCompare(
|
||||
a.attributes.title || a.attributes.friendly_name || "",
|
||||
b.attributes.title || b.attributes.friendly_name || ""
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
private _filterUpdateEntitiesWithInstall = memoizeOne(
|
||||
(entities: HassEntities): [UpdateEntity[], number] => {
|
||||
const updates = this._filterUpdateEntities(entities).filter((entity) =>
|
||||
updateCanInstall(entity)
|
||||
);
|
||||
const updates = filterUpdateEntitiesWithInstall(entities);
|
||||
|
||||
return [
|
||||
updates.slice(0, updates.length === 3 ? updates.length : 2),
|
||||
updates.length,
|
||||
@@ -288,27 +238,9 @@ class HaConfigDashboard extends LitElement {
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
const _entities = this._filterUpdateEntities(this.hass.states).map(
|
||||
(entity) => entity.entity_id
|
||||
);
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
if (_entities.length) {
|
||||
this._notifyUpdates = true;
|
||||
await this.hass.callService("homeassistant", "update_entity", {
|
||||
entity_id: _entities,
|
||||
});
|
||||
return;
|
||||
}
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.updates.no_update_entities.title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.updates.no_update_entities.description"
|
||||
),
|
||||
warning: true,
|
||||
});
|
||||
checkForEntityUpdates(this, this.hass);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
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 { fireEvent } from "../../../common/dom/fire_event";
|
||||
@@ -8,7 +8,7 @@ import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-icon-next";
|
||||
import type { UpdateEntity } from "../../../data/update";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("ha-config-updates")
|
||||
class HaConfigUpdates extends LitElement {
|
||||
@@ -35,39 +35,44 @@ class HaConfigUpdates extends LitElement {
|
||||
count: this.total || this.updateEntities.length,
|
||||
})}
|
||||
</div>
|
||||
${updates.map(
|
||||
(entity) => html`
|
||||
<paper-icon-item
|
||||
@click=${this._openMoreInfo}
|
||||
.entity_id=${entity.entity_id}
|
||||
class=${entity.attributes.skipped_version ? "skipped" : ""}
|
||||
>
|
||||
<span slot="item-icon" class="icon">
|
||||
<mwc-list>
|
||||
${updates.map(
|
||||
(entity) => html`
|
||||
<mwc-list-item
|
||||
twoline
|
||||
graphic="avatar"
|
||||
class=${entity.attributes.skipped_version ? "skipped" : ""}
|
||||
.entity_id=${entity.entity_id}
|
||||
.hasMeta=${!this.narrow}
|
||||
@click=${this._openMoreInfo}
|
||||
>
|
||||
<state-badge
|
||||
slot="graphic"
|
||||
.title=${entity.attributes.title ||
|
||||
entity.attributes.friendly_name}
|
||||
.stateObj=${entity}
|
||||
slot="item-icon"
|
||||
></state-badge>
|
||||
</span>
|
||||
<paper-item-body two-line>
|
||||
${entity.attributes.title || entity.attributes.friendly_name}
|
||||
<div secondary>
|
||||
<span
|
||||
>${entity.attributes.title ||
|
||||
entity.attributes.friendly_name}</span
|
||||
>
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.version_available",
|
||||
{
|
||||
version_available: entity.attributes.latest_version,
|
||||
}
|
||||
)}
|
||||
${entity.attributes.skipped_version
|
||||
)}${entity.attributes.skipped_version
|
||||
? `(${this.hass.localize("ui.panel.config.updates.skipped")})`
|
||||
: ""}
|
||||
</div>
|
||||
</paper-item-body>
|
||||
${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""}
|
||||
</paper-icon-item>
|
||||
`
|
||||
)}
|
||||
</span>
|
||||
${!this.narrow
|
||||
? html`<ha-icon-next slot="meta"></ha-icon-next>`
|
||||
: ""}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -80,6 +85,9 @@ class HaConfigUpdates extends LitElement {
|
||||
static get styles(): CSSResultGroup[] {
|
||||
return [
|
||||
css`
|
||||
:host {
|
||||
--mdc-list-vertical-padding: 0;
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
padding: 16px;
|
||||
@@ -88,11 +96,6 @@ class HaConfigUpdates extends LitElement {
|
||||
.skipped {
|
||||
background: var(--secondary-background-color);
|
||||
}
|
||||
.icon {
|
||||
display: inline-flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
height: 24px;
|
||||
@@ -114,8 +117,9 @@ class HaConfigUpdates extends LitElement {
|
||||
outline: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
paper-icon-item {
|
||||
mwc-list-item {
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -197,7 +197,10 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
),
|
||||
model: device.model || "<unknown>",
|
||||
manufacturer: device.manufacturer || "<unknown>",
|
||||
area: device.area_id ? areaLookup[device.area_id].name : "—",
|
||||
area:
|
||||
device.area_id && areaLookup[device.area_id]
|
||||
? areaLookup[device.area_id].name
|
||||
: "—",
|
||||
integration: device.config_entries.length
|
||||
? device.config_entries
|
||||
.filter((entId) => entId in entryLookup)
|
||||
|
@@ -356,6 +356,25 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
)}
|
||||
</ha-select>`
|
||||
: ""}
|
||||
${this._helperConfigEntry
|
||||
? html`
|
||||
<div class="row">
|
||||
<mwc-button
|
||||
@click=${this._showOptionsFlow}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.configure_state",
|
||||
"integration",
|
||||
domainToName(
|
||||
this.hass.localize,
|
||||
this._helperConfigEntry.domain
|
||||
)
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
error-message="Domain needs to stay the same"
|
||||
.value=${this._entityId}
|
||||
@@ -373,20 +392,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
@value-changed=${this._areaPicked}
|
||||
></ha-area-picker>`
|
||||
: ""}
|
||||
${this._helperConfigEntry
|
||||
? html`
|
||||
<div class="row">
|
||||
<mwc-button
|
||||
@click=${this._showOptionsFlow}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.configure_state"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-expansion-panel
|
||||
.header=${this.hass.localize(
|
||||
|
@@ -20,7 +20,6 @@ import {
|
||||
mdiPuzzle,
|
||||
mdiRobot,
|
||||
mdiScriptText,
|
||||
mdiServer,
|
||||
mdiShape,
|
||||
mdiSofa,
|
||||
mdiTools,
|
||||
@@ -255,14 +254,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
},
|
||||
],
|
||||
general: [
|
||||
{
|
||||
component: "server_control",
|
||||
path: "/config/server_control",
|
||||
translationKey: "ui.panel.config.server_control.caption",
|
||||
iconPath: mdiServer,
|
||||
iconColor: "#4A5963",
|
||||
core: true,
|
||||
},
|
||||
{
|
||||
path: "/config/updates",
|
||||
translationKey: "ui.panel.config.updates.caption",
|
||||
@@ -324,6 +315,13 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
iconColor: "#507FfE",
|
||||
components: ["system_health", "hassio"],
|
||||
},
|
||||
{
|
||||
path: "/config/general",
|
||||
translationKey: "ui.panel.config.core.caption",
|
||||
iconPath: mdiCog,
|
||||
iconColor: "#653249",
|
||||
core: true,
|
||||
},
|
||||
],
|
||||
about: [
|
||||
{
|
||||
@@ -347,6 +345,8 @@ class HaPanelConfig extends HassRouterPage {
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "dashboard",
|
||||
beforeRender: (page) =>
|
||||
page === "server_control" ? "../developer-tools/yaml" : undefined,
|
||||
routes: {
|
||||
analytics: {
|
||||
tag: "ha-config-section-analytics",
|
||||
@@ -438,10 +438,6 @@ 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("./storage/ha-config-section-storage"),
|
||||
@@ -462,6 +458,10 @@ class HaPanelConfig extends HassRouterPage {
|
||||
tag: "ha-config-zone",
|
||||
load: () => import("./zone/ha-config-zone"),
|
||||
},
|
||||
general: {
|
||||
tag: "ha-config-section-general",
|
||||
load: () => import("./core/ha-config-section-general"),
|
||||
},
|
||||
zha: {
|
||||
tag: "zha-config-dashboard-router",
|
||||
load: () =>
|
||||
@@ -540,6 +540,10 @@ class HaPanelConfig extends HassRouterPage {
|
||||
"--app-header-border-bottom",
|
||||
"1px solid var(--divider-color)"
|
||||
);
|
||||
this.style.setProperty(
|
||||
"--ha-card-border-radius",
|
||||
"var(--ha-config-card-border-radius, 8px)"
|
||||
);
|
||||
}
|
||||
|
||||
protected updatePageEl(el) {
|
||||
|
@@ -218,7 +218,7 @@ class HaConfigHardware extends LitElement {
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-card {
|
||||
max-width: 500px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
@@ -237,7 +237,6 @@ class HaConfigHardware extends LitElement {
|
||||
}
|
||||
.card-actions {
|
||||
height: 48px;
|
||||
border-top: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
@@ -79,9 +79,11 @@ class HaInputSelectForm extends LitElement {
|
||||
"ui.dialogs.helper_settings.generic.icon"
|
||||
)}
|
||||
></ha-icon-picker>
|
||||
${this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.input_select.options"
|
||||
)}:
|
||||
<div class="header">
|
||||
${this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.input_select.options"
|
||||
)}:
|
||||
</div>
|
||||
${this._options.length
|
||||
? this._options.map(
|
||||
(option, index) => html`
|
||||
@@ -206,6 +208,10 @@ class HaInputSelectForm extends LitElement {
|
||||
#option_input {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.header {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -67,7 +67,7 @@ class HaConfigInfo extends LitElement {
|
||||
Home Assistant Supervisor ${this._hassioInfo.supervisor}
|
||||
</h2>`
|
||||
: ""}
|
||||
${this._osInfo
|
||||
${this._osInfo?.version
|
||||
? html`<h2>Home Assistant OS ${this._osInfo.version}</h2>`
|
||||
: ""}
|
||||
${this._hostInfo
|
||||
|
@@ -111,7 +111,7 @@ const groupByIntegration = (
|
||||
class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||
|
||||
@property() public isWide!: boolean;
|
||||
|
||||
@@ -709,6 +709,9 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host([narrow]) hass-tabs-subpage {
|
||||
--main-title-margin: 0;
|
||||
}
|
||||
ha-button-menu {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
@@ -12,55 +12,32 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-ansi-to-html";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-select";
|
||||
import { fetchErrorLog, LogProvider } from "../../../data/error_log";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import { fetchErrorLog } from "../../../data/error_log";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import { fetchHassioLogs } from "../../../data/hassio/supervisor";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
const logProviders: LogProvider[] = [
|
||||
{
|
||||
key: "supervisor",
|
||||
name: "Supervisor",
|
||||
},
|
||||
{
|
||||
key: "core",
|
||||
name: "Home Assistant Core",
|
||||
},
|
||||
{
|
||||
key: "host",
|
||||
name: "Host",
|
||||
},
|
||||
{
|
||||
key: "dns",
|
||||
name: "DNS",
|
||||
},
|
||||
{
|
||||
key: "audio",
|
||||
name: "Audio",
|
||||
},
|
||||
{
|
||||
key: "multicast",
|
||||
name: "Multicast",
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("error-log-card")
|
||||
class ErrorLogCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public filter = "";
|
||||
|
||||
@property() public provider!: string;
|
||||
|
||||
@property({ type: Boolean, attribute: true }) public show = false;
|
||||
|
||||
@state() private _isLogLoaded = false;
|
||||
|
||||
@state() private _logHTML!: TemplateResult[] | string;
|
||||
@state() private _logHTML?: TemplateResult[] | TemplateResult | string;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _selectedLogProvider?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="error-log-intro">
|
||||
@@ -71,26 +48,9 @@ class ErrorLogCard extends LitElement {
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="header">
|
||||
${this.hass.userData?.showAdvanced &&
|
||||
isComponentLoaded(this.hass, "hassio")
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.logs.log_provider"
|
||||
)}
|
||||
@selected=${this._setLogProvider}
|
||||
.value=${this._selectedLogProvider}
|
||||
>
|
||||
${logProviders.map(
|
||||
(provider) => html`
|
||||
<mwc-list-item .value=${provider.key}>
|
||||
${provider.name}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
: ""}
|
||||
<h2>
|
||||
${this.hass.localize("ui.panel.config.logs.full_logs")}
|
||||
</h2>
|
||||
<ha-icon-button
|
||||
.path=${mdiRefresh}
|
||||
@click=${this._refresh}
|
||||
@@ -103,8 +63,12 @@ class ErrorLogCard extends LitElement {
|
||||
: ""}
|
||||
${!this._logHTML
|
||||
? html`
|
||||
<mwc-button raised @click=${this._refreshLogs}>
|
||||
${this.hass.localize("ui.panel.config.logs.load_full_log")}
|
||||
<mwc-button
|
||||
raised
|
||||
@click=${this._refreshLogs}
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.logs.load_logs")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
@@ -115,7 +79,7 @@ class ErrorLogCard extends LitElement {
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (this.hass?.config.safe_mode) {
|
||||
if (this.hass?.config.safe_mode || this.show) {
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
this._refreshLogs();
|
||||
}
|
||||
@@ -124,21 +88,19 @@ class ErrorLogCard extends LitElement {
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("filter") && this._isLogLoaded) {
|
||||
if (changedProps.has("provider")) {
|
||||
this._logHTML = undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
(changedProps.has("filter") && this._isLogLoaded) ||
|
||||
(changedProps.has("show") && this.show) ||
|
||||
(changedProps.has("provider") && this.show)
|
||||
) {
|
||||
this._refreshLogs();
|
||||
}
|
||||
}
|
||||
|
||||
private async _setLogProvider(ev): Promise<void> {
|
||||
const provider = ev.target.value;
|
||||
if (provider === this._selectedLogProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._selectedLogProvider = provider;
|
||||
this._refreshLogs();
|
||||
}
|
||||
|
||||
private async _refresh(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
@@ -151,18 +113,18 @@ class ErrorLogCard extends LitElement {
|
||||
this._logHTML = this.hass.localize("ui.panel.config.logs.loading_log");
|
||||
let log: string;
|
||||
|
||||
if (!this._selectedLogProvider && isComponentLoaded(this.hass, "hassio")) {
|
||||
this._selectedLogProvider = "core";
|
||||
}
|
||||
|
||||
if (this._selectedLogProvider) {
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
try {
|
||||
log = await fetchHassioLogs(this.hass, this._selectedLogProvider);
|
||||
log = await fetchHassioLogs(this.hass, this.provider);
|
||||
this._logHTML = html`<ha-ansi-to-html .content=${log}>
|
||||
</ha-ansi-to-html>`;
|
||||
this._isLogLoaded = true;
|
||||
return;
|
||||
} catch (err: any) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.logs.failed_get_logs",
|
||||
"provider",
|
||||
this._selectedLogProvider,
|
||||
this.provider,
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
|
@@ -1,7 +1,11 @@
|
||||
import { mdiChevronDown } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/search-input";
|
||||
import { LogProvider } from "../../../data/error_log";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
@@ -10,22 +14,51 @@ import "./error-log-card";
|
||||
import "./system-log-card";
|
||||
import type { SystemLogCard } from "./system-log-card";
|
||||
|
||||
const logProviders: LogProvider[] = [
|
||||
{
|
||||
key: "core",
|
||||
name: "Home Assistant Core",
|
||||
},
|
||||
{
|
||||
key: "supervisor",
|
||||
name: "Supervisor",
|
||||
},
|
||||
{
|
||||
key: "host",
|
||||
name: "Host",
|
||||
},
|
||||
{
|
||||
key: "dns",
|
||||
name: "DNS",
|
||||
},
|
||||
{
|
||||
key: "audio",
|
||||
name: "Audio",
|
||||
},
|
||||
{
|
||||
key: "multicast",
|
||||
name: "Multicast",
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("ha-config-logs")
|
||||
export class HaConfigLogs extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property() public isWide!: boolean;
|
||||
@property({ type: Boolean }) public isWide!: boolean;
|
||||
|
||||
@property() public showAdvanced!: boolean;
|
||||
@property({ type: Boolean }) public showAdvanced!: boolean;
|
||||
|
||||
@property() public route!: Route;
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _filter = extractSearchParam("filter") || "";
|
||||
|
||||
@query("system-log-card", true) private systemLog?: SystemLogCard;
|
||||
|
||||
@state() private _selectedLogProvider = "core";
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.systemLog && this.systemLog.loaded) {
|
||||
@@ -68,21 +101,60 @@ export class HaConfigLogs extends LitElement {
|
||||
.header=${this.hass.localize("ui.panel.config.logs.caption")}
|
||||
back-path="/config/system"
|
||||
>
|
||||
${isComponentLoaded(this.hass, "hassio") &&
|
||||
this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
|
||||
<mwc-button
|
||||
slot="trigger"
|
||||
.label=${logProviders.find(
|
||||
(p) => p.key === this._selectedLogProvider
|
||||
)!.name}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailingIcon"
|
||||
.path=${mdiChevronDown}
|
||||
></ha-svg-icon>
|
||||
</mwc-button>
|
||||
${logProviders.map(
|
||||
(provider) => html`
|
||||
<mwc-list-item
|
||||
?selected=${provider.key === this._selectedLogProvider}
|
||||
.provider=${provider.key}
|
||||
@click=${this._selectProvider}
|
||||
>
|
||||
${provider.name}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-button-menu>
|
||||
`
|
||||
: ""}
|
||||
${search}
|
||||
<div class="content">
|
||||
<system-log-card
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
></system-log-card>
|
||||
${this._selectedLogProvider === "core"
|
||||
? html`
|
||||
<system-log-card
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
></system-log-card>
|
||||
`
|
||||
: ""}
|
||||
<error-log-card
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.provider=${this._selectedLogProvider}
|
||||
.show=${this._selectedLogProvider !== "core"}
|
||||
></error-log-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _selectProvider(ev) {
|
||||
this._selectedLogProvider = (ev.currentTarget as any).provider;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -108,6 +180,11 @@ export class HaConfigLogs extends LitElement {
|
||||
.content {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
mwc-button[slot="trigger"] {
|
||||
--mdc-theme-primary: var(--primary-text-color);
|
||||
--mdc-icon-size: 36px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ import {
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail";
|
||||
import { formatSystemLogTime } from "./util";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
|
||||
@customElement("system-log-card")
|
||||
export class SystemLogCard extends LitElement {
|
||||
@@ -131,7 +132,7 @@ export class SystemLogCard extends LitElement {
|
||||
`
|
||||
)}
|
||||
|
||||
<div class="card-actions">
|
||||
<div class="card-actions" dir=${computeRTLDirection(this.hass)}>
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
domain="system_log"
|
||||
|
@@ -77,7 +77,6 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${lovelaceTabs}
|
||||
.columns=${this._columns(this.hass.language)}
|
||||
|
@@ -28,6 +28,7 @@ class HaConfigSectionNetwork extends LitElement {
|
||||
${isComponentLoaded(this.hass, "hassio")
|
||||
? html`<supervisor-hostname
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></supervisor-hostname>
|
||||
<supervisor-network .hass=${this.hass}></supervisor-network>`
|
||||
: ""}
|
||||
|
@@ -14,6 +14,7 @@ import "../../../components/ha-header-bar";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-radio";
|
||||
import "../../../components/ha-related-items";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-textfield";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import {
|
||||
@@ -22,12 +23,13 @@ import {
|
||||
} from "../../../data/hassio/host";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-settings-row";
|
||||
|
||||
@customElement("supervisor-hostname")
|
||||
export class HassioHostname extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) narrow!: boolean;
|
||||
|
||||
@state() private _processing = false;
|
||||
|
||||
@state() private _hostname?: string;
|
||||
@@ -48,11 +50,12 @@ export class HassioHostname extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
class="no-padding"
|
||||
outlined
|
||||
.header=${this.hass.localize("ui.panel.config.network.hostname.title")}
|
||||
>
|
||||
<div class="card-content">
|
||||
<ha-settings-row>
|
||||
<div>
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">Hostname</span>
|
||||
<span slot="description"
|
||||
>The name your instance will have on your network</span
|
||||
@@ -98,21 +101,17 @@ export class HassioHostname extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
ha-textfield {
|
||||
width: 100%;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
static styles: CSSResultGroup = css`
|
||||
ha-textfield {
|
||||
width: 100%;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -1,269 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { componentsWithService } from "../../../common/config/components_with_service";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-card";
|
||||
import { checkCoreConfig } from "../../../data/core";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import "../ha-config-section";
|
||||
|
||||
@customElement("ha-config-server-control")
|
||||
export class HaConfigServerControl extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public isWide!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public showAdvanced!: boolean;
|
||||
|
||||
@state() private _validating = false;
|
||||
|
||||
@state() private _reloadableDomains: string[] = [];
|
||||
|
||||
private _validateLog = "";
|
||||
|
||||
private _isValid: boolean | null = null;
|
||||
|
||||
protected updated(changedProperties) {
|
||||
const oldHass = changedProperties.get("hass");
|
||||
if (
|
||||
changedProperties.has("hass") &&
|
||||
(!oldHass || oldHass.config.components !== this.hass.config.components)
|
||||
) {
|
||||
this._reloadableDomains = componentsWithService(
|
||||
this.hass,
|
||||
"reload"
|
||||
).sort();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/system"
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.header=${this.hass.localize("ui.panel.config.server_control.caption")}
|
||||
>
|
||||
<div class="content">
|
||||
${this.showAdvanced
|
||||
? html`
|
||||
<ha-card
|
||||
outlined
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.validation.heading"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.validation.introduction"
|
||||
)}
|
||||
${!this._validateLog
|
||||
? html`
|
||||
<div
|
||||
class="validate-container layout vertical center-center"
|
||||
>
|
||||
${!this._validating
|
||||
? html`
|
||||
${this._isValid
|
||||
? html` <div
|
||||
class="validate-result"
|
||||
id="result"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.validation.valid"
|
||||
)}
|
||||
</div>`
|
||||
: ""}
|
||||
<mwc-button
|
||||
raised
|
||||
@click=${this._validateConfig}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.validation.check_config"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: html`
|
||||
<ha-circular-progress
|
||||
active
|
||||
></ha-circular-progress>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="config-invalid">
|
||||
<span class="text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.validation.invalid"
|
||||
)}
|
||||
</span>
|
||||
<mwc-button raised @click=${this._validateConfig}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.validation.check_config"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
<div id="configLog" class="validate-log">
|
||||
${this._validateLog}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-card
|
||||
outlined
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.heading"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.introduction"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-actions warning">
|
||||
<ha-call-service-button
|
||||
class="warning"
|
||||
.hass=${this.hass}
|
||||
domain="homeassistant"
|
||||
service="restart"
|
||||
.confirmation=${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.confirm_restart"
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.server_management.restart"
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
${this.showAdvanced
|
||||
? html`
|
||||
<ha-card
|
||||
outlined
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.reloading.heading"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.reloading.introduction"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
domain="homeassistant"
|
||||
service="reload_core_config"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.server_control.section.reloading.core"
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
${this._reloadableDomains.map(
|
||||
(domain) =>
|
||||
html`
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
service="reload"
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.server_control.section.reloading.${domain}`
|
||||
) ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.server_control.section.reloading.reload",
|
||||
"domain",
|
||||
domainToName(this.hass.localize, domain)
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _validateConfig() {
|
||||
this._validating = true;
|
||||
this._validateLog = "";
|
||||
this._isValid = null;
|
||||
|
||||
const configCheck = await checkCoreConfig(this.hass);
|
||||
this._validating = false;
|
||||
this._isValid = configCheck.result === "valid";
|
||||
|
||||
if (configCheck.errors) {
|
||||
this._validateLog = configCheck.errors;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.validate-container {
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.validate-result {
|
||||
color: var(--success-color);
|
||||
font-weight: 500;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.config-invalid {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.config-invalid .text {
|
||||
color: var(--error-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.config-invalid mwc-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.validate-log {
|
||||
white-space: pre-line;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin-top: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-server-control": HaConfigServerControl;
|
||||
}
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
@@ -40,6 +41,22 @@ class HaConfigSectionStorage extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.storage.caption")}
|
||||
>
|
||||
${this._hostInfo
|
||||
? html`
|
||||
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.overflow")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<mwc-list-item @click=${this._moveDatadisk}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.storage.datadisk.title"
|
||||
)}
|
||||
</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
`
|
||||
: ""}
|
||||
<div class="content">
|
||||
${this._error
|
||||
? html`
|
||||
@@ -79,13 +96,6 @@ class HaConfigSectionStorage extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._moveDatadisk}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.storage.datadisk.title"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
@@ -118,26 +128,16 @@ class HaConfigSectionStorage extends LitElement {
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-card {
|
||||
max-width: 500px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
.card-actions {
|
||||
height: 48px;
|
||||
border-top: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -14,15 +14,21 @@ import "../../../components/ha-card";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-metric";
|
||||
import { fetchHassioStats, HassioStats } from "../../../data/hassio/common";
|
||||
import {
|
||||
fetchHassioResolution,
|
||||
HassioResolution,
|
||||
} from "../../../data/hassio/resolution";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import {
|
||||
subscribeSystemHealthInfo,
|
||||
SystemCheckValueObject,
|
||||
SystemHealthInfo,
|
||||
} from "../../../data/system_health";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
|
||||
const sortKeys = (a: string, b: string) => {
|
||||
@@ -41,6 +47,11 @@ const sortKeys = (a: string, b: string) => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const UNSUPPORTED_REASON_URL = {};
|
||||
export const UNHEALTHY_REASON_URL = {
|
||||
privileged: "/more-info/unsupported/privileged",
|
||||
};
|
||||
|
||||
@customElement("ha-config-system-health")
|
||||
class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -51,6 +62,8 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _supervisorStats?: HassioStats;
|
||||
|
||||
@state() private _resolutionInfo?: HassioResolution;
|
||||
|
||||
@state() private _coreStats?: HassioStats;
|
||||
|
||||
@state() private _error?: { code: string; message: string };
|
||||
@@ -79,6 +92,9 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
|
||||
10000
|
||||
)
|
||||
);
|
||||
fetchHassioResolution(this.hass).then((data) => {
|
||||
this._resolutionInfo = data;
|
||||
});
|
||||
}
|
||||
|
||||
return subs;
|
||||
@@ -219,6 +235,35 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
|
||||
`
|
||||
: ""}
|
||||
<div class="content">
|
||||
${this._resolutionInfo
|
||||
? html`${this._resolutionInfo.unhealthy.length
|
||||
? html`<ha-alert alert-type="error">
|
||||
${this.hass.localize("ui.dialogs.unhealthy.title")}
|
||||
<mwc-button
|
||||
slot="action"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.common.learn_more"
|
||||
)}
|
||||
@click=${this._unhealthyDialog}
|
||||
>
|
||||
</mwc-button
|
||||
></ha-alert>`
|
||||
: ""}
|
||||
${this._resolutionInfo.unsupported.length
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.hass.localize("ui.dialogs.unsupported.title")}
|
||||
<mwc-button
|
||||
slot="action"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.common.learn_more"
|
||||
)}
|
||||
@click=${this._unsupportedDialog}
|
||||
>
|
||||
</mwc-button>
|
||||
</ha-alert>`
|
||||
: ""} `
|
||||
: ""}
|
||||
|
||||
<ha-card outlined>
|
||||
<div class="card-content">${sections}</div>
|
||||
</ha-card>
|
||||
@@ -277,6 +322,64 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _unsupportedDialog(): Promise<void> {
|
||||
await showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.dialogs.unsupported.title"),
|
||||
text: html`${this.hass.localize("ui.dialogs.unsupported.description")}
|
||||
<br /><br />
|
||||
<ul>
|
||||
${this._resolutionInfo!.unsupported.map(
|
||||
(reason) => html`
|
||||
<li>
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
UNSUPPORTED_REASON_URL[reason] ||
|
||||
`/more-info/unsupported/${reason}`
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.unsupported.reason.${reason}`
|
||||
) || reason}
|
||||
</a>
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _unhealthyDialog(): Promise<void> {
|
||||
await showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.dialogs.unhealthy.title"),
|
||||
text: html`${this.hass.localize("ui.dialogs.unhealthy.description")}
|
||||
<br /><br />
|
||||
<ul>
|
||||
${this._resolutionInfo!.unhealthy.map(
|
||||
(reason) => html`
|
||||
<li>
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
UNHEALTHY_REASON_URL[reason] ||
|
||||
`/more-info/unhealthy/${reason}`
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.unhealthy.reason.${reason}`
|
||||
) || reason}
|
||||
</a>
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _copyInfo(ev: CustomEvent<ActionDetail>): Promise<void> {
|
||||
const github = ev.detail.index === 1;
|
||||
let haContent: string | undefined;
|
||||
@@ -349,11 +452,17 @@ class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
ha-card {
|
||||
display: block;
|
||||
max-width: 500px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: max(24px, env(safe-area-inset-bottom));
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: max(24px, env(safe-area-inset-bottom));
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
@@ -1,284 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import timezones from "google-timezones-json";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { UNIT_C } from "../../../common/const";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { currencies } from "../../../components/currency-datalist";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../components/ha-radio";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-textfield";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||
import { SYMBOL_TO_ISO } from "../../../data/currency";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("dialog-core-zone-detail")
|
||||
class DialogZoneDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
|
||||
|
||||
@state() private _currency?: string;
|
||||
|
||||
@state() private _name?: string;
|
||||
|
||||
@state() private _elevation?: number;
|
||||
|
||||
@state() private _timeZone?: string;
|
||||
|
||||
public showDialog(): void {
|
||||
this._submitting = false;
|
||||
this._unitSystem =
|
||||
this.hass.config.unit_system.temperature === UNIT_C
|
||||
? "metric"
|
||||
: "imperial";
|
||||
this._currency = this.hass.config.currency;
|
||||
this._elevation = this.hass.config.elevation;
|
||||
this._timeZone = this.hass.config.time_zone;
|
||||
this._name = this.hass.config.location_name;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
this._currency = undefined;
|
||||
this._elevation = undefined;
|
||||
this._timeZone = undefined;
|
||||
this._unitSystem = undefined;
|
||||
this._name = undefined;
|
||||
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canEdit = ["storage", "default"].includes(
|
||||
this.hass.config.config_source
|
||||
);
|
||||
const disabled = this._submitting || !canEdit;
|
||||
|
||||
if (!this._open) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.panel.config.zone.core_location_dialog")
|
||||
)}
|
||||
>
|
||||
${!canEdit
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
name="name"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.location_name"
|
||||
)}
|
||||
.disabled=${disabled}
|
||||
.value=${this._name}
|
||||
@change=${this._handleChange}
|
||||
></ha-textfield>
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.time_zone"
|
||||
)}
|
||||
name="timeZone"
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.disabled=${disabled}
|
||||
.value=${this._timeZone}
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
${Object.keys(timezones).map(
|
||||
(tz) =>
|
||||
html`<mwc-list-item value=${tz}>${timezones[tz]}</mwc-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation"
|
||||
)}
|
||||
name="elevation"
|
||||
type="number"
|
||||
.disabled=${disabled}
|
||||
.value=${this._elevation}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
<span slot="suffix">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation_meters"
|
||||
)}
|
||||
</span>
|
||||
</ha-textfield>
|
||||
<div>
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system"
|
||||
)}
|
||||
</div>
|
||||
<ha-formfield
|
||||
.label=${html`${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_metric"
|
||||
)}
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.metric_example"
|
||||
)}
|
||||
</div>`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="metric"
|
||||
.checked=${this._unitSystem === "metric"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${this._submitting}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_imperial"
|
||||
)}
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.imperial_example"
|
||||
)}
|
||||
</div>`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="imperial"
|
||||
.checked=${this._unitSystem === "imperial"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${this._submitting}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<div>
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.currency"
|
||||
)}
|
||||
name="currency"
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.disabled=${disabled}
|
||||
.value=${this._currency}
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
${currencies.map(
|
||||
(currency) =>
|
||||
html`<mwc-list-item .value=${currency}
|
||||
>${currency}</mwc-list-item
|
||||
>`
|
||||
)}</ha-select
|
||||
>
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.find_currency_value"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
<mwc-button slot="primaryAction" @click=${this._updateEntry}>
|
||||
${this.hass!.localize("ui.panel.config.zone.detail.update")}
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
const target = ev.currentTarget;
|
||||
let value = target.value;
|
||||
|
||||
if (target.name === "currency" && value) {
|
||||
if (value in SYMBOL_TO_ISO) {
|
||||
value = SYMBOL_TO_ISO[value];
|
||||
}
|
||||
}
|
||||
|
||||
this[`_${target.name}`] = value;
|
||||
}
|
||||
|
||||
private _unitSystemChanged(ev: CustomEvent) {
|
||||
this._unitSystem = (ev.target as HaRadio).value as "metric" | "imperial";
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
await saveCoreConfig(this.hass, {
|
||||
currency: this._currency,
|
||||
elevation: Number(this._elevation),
|
||||
unit_system: this._unitSystem,
|
||||
time_zone: this._timeZone,
|
||||
location_name: this._name,
|
||||
});
|
||||
} catch (err: any) {
|
||||
alert(`Error saving config: ${err.message}`);
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 600px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: calc(
|
||||
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
|
||||
);
|
||||
}
|
||||
}
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
ha-dialog > * {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-select {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-core-zone-detail": DialogZoneDetail;
|
||||
}
|
||||
}
|
@@ -46,7 +46,6 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showCoreZoneDetailDialog } from "./show-dialog-core-zone-detail";
|
||||
import { showZoneDetailDialog } from "./show-dialog-zone-detail";
|
||||
|
||||
@customElement("ha-config-zone")
|
||||
@@ -188,30 +187,26 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
<div style="display:inline-block">
|
||||
<ha-icon-button
|
||||
.entityId=${stateObject.entity_id}
|
||||
@click=${this._openCoreConfig}
|
||||
.noEdit=${stateObject.entity_id !== "zone.home" ||
|
||||
!this._canEditCore}
|
||||
.path=${stateObject.entity_id === "zone.home" &&
|
||||
this._canEditCore
|
||||
? mdiPencil
|
||||
: mdiPencilOff}
|
||||
.label=${hass.localize(
|
||||
"ui.panel.config.zone.edit_zone"
|
||||
)}
|
||||
.label=${stateObject.entity_id === "zone.home"
|
||||
? hass.localize("ui.panel.config.zone.edit_home")
|
||||
: hass.localize("ui.panel.config.zone.edit_zone")}
|
||||
@click=${this._openCoreConfig}
|
||||
></ha-icon-button>
|
||||
<paper-tooltip animation-delay="0" position="left">
|
||||
${stateObject.entity_id === "zone.home"
|
||||
? hass.localize(
|
||||
`ui.panel.config.zone.${
|
||||
this.narrow
|
||||
? "edit_home_zone_narrow"
|
||||
: "edit_home_zone"
|
||||
}`
|
||||
)
|
||||
: hass.localize(
|
||||
"ui.panel.config.zone.configured_in_yaml"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
${stateObject.entity_id !== "zone.home"
|
||||
? html`
|
||||
<paper-tooltip animation-delay="0" position="left">
|
||||
${hass.localize(
|
||||
"ui.panel.config.zone.configured_in_yaml"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</paper-icon-item>
|
||||
`
|
||||
@@ -397,7 +392,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
showCoreZoneDetailDialog(this);
|
||||
navigate("/config/general");
|
||||
}
|
||||
|
||||
private async _createEntry(values: ZoneMutableParams) {
|
||||
|
@@ -1,12 +0,0 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
export const loadCoreZoneDetailDialog = () =>
|
||||
import("./dialog-core-zone-detail");
|
||||
|
||||
export const showCoreZoneDetailDialog = (element: HTMLElement): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-core-zone-detail",
|
||||
dialogImport: loadCoreZoneDetailDialog,
|
||||
dialogParams: {},
|
||||
});
|
||||
};
|
@@ -41,6 +41,10 @@ class DeveloperToolsRouter extends HassRouterPage {
|
||||
tag: "developer-tools-statistics",
|
||||
load: () => import("./statistics/developer-tools-statistics"),
|
||||
},
|
||||
yaml: {
|
||||
tag: "developer-yaml-config",
|
||||
load: () => import("./yaml_configuration/developer-yaml-config"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -67,6 +67,9 @@ class PanelDeveloperTools extends LitElement {
|
||||
"ui.panel.developer-tools.tabs.statistics.title"
|
||||
)}
|
||||
</paper-tab>
|
||||
<paper-tab page-name="yaml">
|
||||
${this.hass.localize("ui.panel.developer-tools.tabs.yaml.title")}
|
||||
</paper-tab>
|
||||
</ha-tabs>
|
||||
</app-header>
|
||||
<developer-tools-router
|
||||
|
@@ -0,0 +1,232 @@
|
||||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { componentsWithService } from "../../../common/config/components_with_service";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import { checkCoreConfig } from "../../../data/core";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
|
||||
@customElement("developer-yaml-config")
|
||||
export class DeveloperYamlConfig extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public isWide!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public showAdvanced!: boolean;
|
||||
|
||||
@state() private _validating = false;
|
||||
|
||||
@state() private _reloadableDomains: string[] = [];
|
||||
|
||||
private _validateLog = "";
|
||||
|
||||
private _isValid: boolean | null = null;
|
||||
|
||||
protected updated(changedProperties) {
|
||||
const oldHass = changedProperties.get("hass");
|
||||
if (
|
||||
changedProperties.has("hass") &&
|
||||
(!oldHass || oldHass.config.components !== this.hass.config.components)
|
||||
) {
|
||||
this._reloadableDomains = componentsWithService(
|
||||
this.hass,
|
||||
"reload"
|
||||
).sort();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-card
|
||||
outlined
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.validation.heading"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.validation.introduction"
|
||||
)}
|
||||
${!this._validateLog
|
||||
? html`
|
||||
<div class="validate-container layout vertical center-center">
|
||||
${!this._validating
|
||||
? html`
|
||||
${this._isValid
|
||||
? html`<div class="validate-result" id="result">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.validation.valid"
|
||||
)}
|
||||
</div>`
|
||||
: ""}
|
||||
`
|
||||
: html`
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="config-invalid">
|
||||
<span class="text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.validation.invalid"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div id="configLog" class="validate-log">
|
||||
${this._validateLog}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._validateConfig}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.validation.check_config"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
class="warning"
|
||||
@click=${this._restart}
|
||||
.disabled=${this._validateLog}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.server_management.restart"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
outlined
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.reloading.heading"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.reloading.introduction"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
domain="homeassistant"
|
||||
service="reload_core_config"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.reloading.core"
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
${this._reloadableDomains.map(
|
||||
(domain) =>
|
||||
html`
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
service="reload"
|
||||
>${this.hass.localize(
|
||||
`ui.panel.developer-tools.tabs.yaml.section.reloading.${domain}`
|
||||
) ||
|
||||
this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.reloading.reload",
|
||||
"domain",
|
||||
domainToName(this.hass.localize, domain)
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _validateConfig() {
|
||||
this._validating = true;
|
||||
this._validateLog = "";
|
||||
this._isValid = null;
|
||||
|
||||
const configCheck = await checkCoreConfig(this.hass);
|
||||
this._validating = false;
|
||||
this._isValid = configCheck.result === "valid";
|
||||
|
||||
if (configCheck.errors) {
|
||||
this._validateLog = configCheck.errors;
|
||||
}
|
||||
}
|
||||
|
||||
private _restart() {
|
||||
showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.yaml.section.server_management.confirm_restart"
|
||||
),
|
||||
confirmText: this.hass!.localize("ui.common.leave"),
|
||||
dismissText: this.hass!.localize("ui.common.stay"),
|
||||
confirm: () => {
|
||||
this.hass.callService("homeassistant", "restart");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.validate-container {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.validate-result {
|
||||
color: var(--success-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.config-invalid {
|
||||
margin: 1em 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.config-invalid .text {
|
||||
color: var(--error-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.validate-log {
|
||||
white-space: pre-line;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 28px 20px 16px;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"developer-yaml-config": DeveloperYamlConfig;
|
||||
}
|
||||
}
|
@@ -12,172 +12,183 @@ import "../../layouts/hass-error-screen";
|
||||
import { HomeAssistant, Route } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
|
||||
export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({
|
||||
developer_states: {
|
||||
redirect: "/developer-tools/state",
|
||||
},
|
||||
developer_services: {
|
||||
redirect: "/developer-tools/service",
|
||||
},
|
||||
developer_call_service: {
|
||||
redirect: "/developer-tools/service",
|
||||
params: {
|
||||
service: "string",
|
||||
},
|
||||
},
|
||||
developer_template: {
|
||||
redirect: "/developer-tools/template",
|
||||
},
|
||||
developer_events: {
|
||||
redirect: "/developer-tools/event",
|
||||
},
|
||||
developer_statistics: {
|
||||
redirect: "/developer-tools/statistics",
|
||||
},
|
||||
config: {
|
||||
redirect: "/config/dashboard",
|
||||
},
|
||||
cloud: {
|
||||
component: "cloud",
|
||||
redirect: "/config/cloud",
|
||||
},
|
||||
config_flow_start: {
|
||||
redirect: "/config/integrations/add",
|
||||
params: {
|
||||
domain: "string",
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
redirect: "/config/integrations",
|
||||
},
|
||||
config_mqtt: {
|
||||
component: "mqtt",
|
||||
redirect: "/config/mqtt",
|
||||
},
|
||||
config_zha: {
|
||||
component: "zha",
|
||||
redirect: "/config/zha/dashboard",
|
||||
},
|
||||
config_zwave_js: {
|
||||
component: "zwave_js",
|
||||
redirect: "/config/zwave_js/dashboard",
|
||||
},
|
||||
config_energy: {
|
||||
component: "energy",
|
||||
redirect: "/config/energy/dashboard",
|
||||
},
|
||||
devices: {
|
||||
redirect: "/config/devices/dashboard",
|
||||
},
|
||||
entities: {
|
||||
redirect: "/config/entities",
|
||||
},
|
||||
energy: {
|
||||
component: "energy",
|
||||
redirect: "/energy",
|
||||
},
|
||||
areas: {
|
||||
redirect: "/config/areas/dashboard",
|
||||
},
|
||||
blueprint_import: {
|
||||
component: "blueprint",
|
||||
redirect: "/config/blueprint/dashboard/import",
|
||||
params: {
|
||||
blueprint_url: "url",
|
||||
},
|
||||
},
|
||||
blueprints: {
|
||||
component: "blueprint",
|
||||
redirect: "/config/blueprint/dashboard",
|
||||
},
|
||||
automations: {
|
||||
component: "automation",
|
||||
redirect: "/config/automation/dashboard",
|
||||
},
|
||||
scenes: {
|
||||
component: "scene",
|
||||
redirect: "/config/scene/dashboard",
|
||||
},
|
||||
scripts: {
|
||||
component: "script",
|
||||
redirect: "/config/script/dashboard",
|
||||
},
|
||||
helpers: {
|
||||
redirect: "/config/helpers",
|
||||
},
|
||||
tags: {
|
||||
component: "tag",
|
||||
redirect: "/config/tags",
|
||||
},
|
||||
lovelace_dashboards: {
|
||||
component: "lovelace",
|
||||
redirect: "/config/lovelace/dashboards",
|
||||
},
|
||||
lovelace_resources: {
|
||||
component: "lovelace",
|
||||
redirect: "/config/lovelace/resources",
|
||||
},
|
||||
people: {
|
||||
component: "person",
|
||||
redirect: "/config/person",
|
||||
},
|
||||
zones: {
|
||||
component: "zone",
|
||||
redirect: "/config/zone",
|
||||
},
|
||||
users: {
|
||||
redirect: "/config/users",
|
||||
},
|
||||
general: {
|
||||
redirect: "/config/core",
|
||||
},
|
||||
server_controls: {
|
||||
redirect: "/developer-tools/yaml",
|
||||
},
|
||||
logs: {
|
||||
redirect: "/config/logs",
|
||||
},
|
||||
info: {
|
||||
redirect: "/config/info",
|
||||
},
|
||||
customize: {
|
||||
// customize was removed in 2021.12, fallback to dashboard
|
||||
redirect: "/config/dashboard",
|
||||
},
|
||||
profile: {
|
||||
redirect: "/profile",
|
||||
},
|
||||
logbook: {
|
||||
component: "logbook",
|
||||
redirect: "/logbook",
|
||||
},
|
||||
history: {
|
||||
component: "history",
|
||||
redirect: "/history",
|
||||
},
|
||||
media_browser: {
|
||||
component: "media_source",
|
||||
redirect: "/media-browser",
|
||||
},
|
||||
backup: {
|
||||
component: hasSupervisor ? "hassio" : "backup",
|
||||
redirect: hasSupervisor ? "/hassio/backups" : "/config/backup",
|
||||
},
|
||||
supervisor_snapshots: {
|
||||
component: hasSupervisor ? "hassio" : "backup",
|
||||
redirect: hasSupervisor ? "/hassio/backups" : "/config/backup",
|
||||
},
|
||||
supervisor_backups: {
|
||||
component: hasSupervisor ? "hassio" : "backup",
|
||||
redirect: hasSupervisor ? "/hassio/backups" : "/config/backup",
|
||||
},
|
||||
supervisor_system: {
|
||||
// Moved from Supervisor panel in 2022.5
|
||||
redirect: "/config/system",
|
||||
},
|
||||
supervisor_logs: {
|
||||
// Moved from Supervisor panel in 2022.5
|
||||
redirect: "/config/logs",
|
||||
},
|
||||
supervisor_info: {
|
||||
// Moved from Supervisor panel in 2022.5
|
||||
redirect: "/config/info",
|
||||
},
|
||||
});
|
||||
|
||||
const getRedirect = (
|
||||
path: string,
|
||||
hasSupervisor: boolean
|
||||
): Redirect | undefined =>
|
||||
((
|
||||
{
|
||||
developer_states: {
|
||||
redirect: "/developer-tools/state",
|
||||
},
|
||||
developer_services: {
|
||||
redirect: "/developer-tools/service",
|
||||
},
|
||||
developer_call_service: {
|
||||
redirect: "/developer-tools/service",
|
||||
params: {
|
||||
service: "string",
|
||||
},
|
||||
},
|
||||
developer_template: {
|
||||
redirect: "/developer-tools/template",
|
||||
},
|
||||
developer_events: {
|
||||
redirect: "/developer-tools/event",
|
||||
},
|
||||
developer_statistics: {
|
||||
redirect: "/developer-tools/statistics",
|
||||
},
|
||||
config: {
|
||||
redirect: "/config",
|
||||
},
|
||||
cloud: {
|
||||
component: "cloud",
|
||||
redirect: "/config/cloud",
|
||||
},
|
||||
integrations: {
|
||||
redirect: "/config/integrations",
|
||||
},
|
||||
config_flow_start: {
|
||||
redirect: "/config/integrations/add",
|
||||
params: {
|
||||
domain: "string",
|
||||
},
|
||||
},
|
||||
config_mqtt: {
|
||||
component: "mqtt",
|
||||
redirect: "/config/mqtt",
|
||||
},
|
||||
config_zha: {
|
||||
component: "zha",
|
||||
redirect: "/config/zha/dashboard",
|
||||
},
|
||||
config_zwave_js: {
|
||||
component: "zwave_js",
|
||||
redirect: "/config/zwave_js/dashboard",
|
||||
},
|
||||
config_energy: {
|
||||
component: "energy",
|
||||
redirect: "/config/energy/dashboard",
|
||||
},
|
||||
devices: {
|
||||
redirect: "/config/devices/dashboard",
|
||||
},
|
||||
entities: {
|
||||
redirect: "/config/entities",
|
||||
},
|
||||
energy: {
|
||||
component: "energy",
|
||||
redirect: "/energy",
|
||||
},
|
||||
areas: {
|
||||
redirect: "/config/areas/dashboard",
|
||||
},
|
||||
blueprints: {
|
||||
component: "blueprint",
|
||||
redirect: "/config/blueprint/dashboard",
|
||||
},
|
||||
blueprint_import: {
|
||||
component: "blueprint",
|
||||
redirect: "/config/blueprint/dashboard/import",
|
||||
params: {
|
||||
blueprint_url: "url",
|
||||
},
|
||||
},
|
||||
automations: {
|
||||
component: "automation",
|
||||
redirect: "/config/automation/dashboard",
|
||||
},
|
||||
scenes: {
|
||||
component: "scene",
|
||||
redirect: "/config/scene/dashboard",
|
||||
},
|
||||
scripts: {
|
||||
component: "script",
|
||||
redirect: "/config/script/dashboard",
|
||||
},
|
||||
helpers: {
|
||||
redirect: "/config/helpers",
|
||||
},
|
||||
tags: {
|
||||
component: "tag",
|
||||
redirect: "/config/tags",
|
||||
},
|
||||
lovelace_dashboards: {
|
||||
component: "lovelace",
|
||||
redirect: "/config/lovelace/dashboards",
|
||||
},
|
||||
lovelace_resources: {
|
||||
component: "lovelace",
|
||||
redirect: "/config/lovelace/resources",
|
||||
},
|
||||
people: {
|
||||
component: "person",
|
||||
redirect: "/config/person",
|
||||
},
|
||||
zones: {
|
||||
component: "zone",
|
||||
redirect: "/config/zone",
|
||||
},
|
||||
users: {
|
||||
redirect: "/config/users",
|
||||
},
|
||||
general: {
|
||||
redirect: "/config/core",
|
||||
},
|
||||
server_controls: {
|
||||
redirect: "/config/server_control",
|
||||
},
|
||||
logs: {
|
||||
redirect: "/config/logs",
|
||||
},
|
||||
info: {
|
||||
redirect: "/config/info",
|
||||
},
|
||||
customize: {
|
||||
// customize was removed in 2021.12, fallback to dashboard
|
||||
redirect: "/config/dashboard",
|
||||
},
|
||||
profile: {
|
||||
redirect: "/profile/dashboard",
|
||||
},
|
||||
logbook: {
|
||||
component: "logbook",
|
||||
redirect: "/logbook",
|
||||
},
|
||||
history: {
|
||||
component: "history",
|
||||
redirect: "/history",
|
||||
},
|
||||
media_browser: {
|
||||
component: "media_source",
|
||||
redirect: "/media-browser",
|
||||
},
|
||||
backup: {
|
||||
component: hasSupervisor ? "hassio" : "backup",
|
||||
redirect: hasSupervisor ? "/hassio/backups" : "/config/backup",
|
||||
},
|
||||
supervisor_snapshots: {
|
||||
component: hasSupervisor ? "hassio" : "backup",
|
||||
redirect: hasSupervisor ? "/hassio/backups" : "/config/backup",
|
||||
},
|
||||
supervisor_backups: {
|
||||
component: hasSupervisor ? "hassio" : "backup",
|
||||
redirect: hasSupervisor ? "/hassio/backups" : "/config/backup",
|
||||
},
|
||||
} as Redirects
|
||||
)[path]);
|
||||
): Redirect | undefined => getMyRedirects(hasSupervisor)?.[path];
|
||||
|
||||
export type ParamType = "url" | "string";
|
||||
|
||||
|
@@ -30,7 +30,8 @@ class StateCardInputNumber extends mixinBehaviors(
|
||||
.sliderstate {
|
||||
min-width: 45px;
|
||||
}
|
||||
ha-slider[hidden] {
|
||||
ha-slider[hidden],
|
||||
ha-textfield[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
ha-textfield {
|
||||
|
@@ -1,16 +1,20 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import tinykeys from "tinykeys";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import {
|
||||
QuickBarParams,
|
||||
showQuickBar,
|
||||
} from "../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { Constructor, HomeAssistant } from "../types";
|
||||
import { storeState } from "../util/ha-pref-storage";
|
||||
import { showToast } from "../util/toast";
|
||||
import { HassElement } from "./hass-element";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"hass-quick-bar": QuickBarParams;
|
||||
"hass-quick-bar-trigger": KeyboardEvent;
|
||||
"hass-enable-shortcuts": HomeAssistant["enableShortcuts"];
|
||||
}
|
||||
}
|
||||
@@ -25,6 +29,20 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
storeState(this.hass!);
|
||||
});
|
||||
|
||||
mainWindow.addEventListener("hass-quick-bar-trigger", (ev) => {
|
||||
switch (ev.detail.key) {
|
||||
case "e":
|
||||
this._showQuickBar(ev.detail);
|
||||
break;
|
||||
case "c":
|
||||
this._showQuickBar(ev.detail, true);
|
||||
break;
|
||||
case "m":
|
||||
this._createMyLink(ev.detail);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this._registerShortcut();
|
||||
}
|
||||
|
||||
@@ -32,6 +50,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
tinykeys(window, {
|
||||
e: (ev) => this._showQuickBar(ev),
|
||||
c: (ev) => this._showQuickBar(ev, true),
|
||||
m: (ev) => this._createMyLink(ev),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,6 +62,63 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
showQuickBar(this, { commandMode });
|
||||
}
|
||||
|
||||
private async _createMyLink(e: KeyboardEvent) {
|
||||
if (
|
||||
!this.hass?.enableShortcuts ||
|
||||
!this._canOverrideAlphanumericInput(e)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetPath = mainWindow.location.pathname;
|
||||
const isHassio = isComponentLoaded(this.hass, "hassio");
|
||||
const myParams = new URLSearchParams();
|
||||
|
||||
if (isHassio && targetPath.startsWith("/hassio")) {
|
||||
const myPanelSupervisor = await import(
|
||||
"../../hassio/src/hassio-my-redirect"
|
||||
);
|
||||
for (const [slug, redirect] of Object.entries(
|
||||
myPanelSupervisor.REDIRECTS
|
||||
)) {
|
||||
if (targetPath.startsWith(redirect.redirect)) {
|
||||
myParams.append("redirect", slug);
|
||||
if (redirect.redirect === "/hassio/addon") {
|
||||
myParams.append("addon", targetPath.split("/")[3]);
|
||||
}
|
||||
window.open(
|
||||
`https://my.home-assistant.io/create-link/?${myParams.toString()}`,
|
||||
"_blank"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const myPanel = await import("../panels/my/ha-panel-my");
|
||||
|
||||
for (const [slug, redirect] of Object.entries(
|
||||
myPanel.getMyRedirects(isHassio)
|
||||
)) {
|
||||
if (targetPath.startsWith(redirect.redirect)) {
|
||||
myParams.append("redirect", slug);
|
||||
window.open(
|
||||
`https://my.home-assistant.io/create-link/?${myParams.toString()}`,
|
||||
"_blank"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.notification_toast.no_matching_link_found",
|
||||
{
|
||||
path: targetPath,
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private _canShowQuickBar(e: KeyboardEvent) {
|
||||
return (
|
||||
this.hass?.user?.is_admin &&
|
||||
|
@@ -314,6 +314,7 @@
|
||||
"undo": "Undo",
|
||||
"move": "Move",
|
||||
"save": "Save",
|
||||
"edit": "Edit",
|
||||
"submit": "Submit",
|
||||
"rename": "Rename",
|
||||
"yes": "Yes",
|
||||
@@ -624,43 +625,43 @@
|
||||
"quick-bar": {
|
||||
"commands": {
|
||||
"reload": {
|
||||
"reload": "[%key:ui::panel::config::server_control::section::reloading::reload%]",
|
||||
"core": "[%key:ui::panel::config::server_control::section::reloading::core%]",
|
||||
"group": "[%key:ui::panel::config::server_control::section::reloading::group%]",
|
||||
"automation": "[%key:ui::panel::config::server_control::section::reloading::automation%]",
|
||||
"script": "[%key:ui::panel::config::server_control::section::reloading::script%]",
|
||||
"scene": "[%key:ui::panel::config::server_control::section::reloading::scene%]",
|
||||
"person": "[%key:ui::panel::config::server_control::section::reloading::person%]",
|
||||
"zone": "[%key:ui::panel::config::server_control::section::reloading::zone%]",
|
||||
"input_boolean": "[%key:ui::panel::config::server_control::section::reloading::input_boolean%]",
|
||||
"input_text": "[%key:ui::panel::config::server_control::section::reloading::input_text%]",
|
||||
"input_number": "[%key:ui::panel::config::server_control::section::reloading::input_number%]",
|
||||
"input_datetime": "[%key:ui::panel::config::server_control::section::reloading::input_datetime%]",
|
||||
"input_select": "[%key:ui::panel::config::server_control::section::reloading::input_select%]",
|
||||
"template": "[%key:ui::panel::config::server_control::section::reloading::template%]",
|
||||
"universal": "[%key:ui::panel::config::server_control::section::reloading::universal%]",
|
||||
"rest": "[%key:ui::panel::config::server_control::section::reloading::rest%]",
|
||||
"command_line": "[%key:ui::panel::config::server_control::section::reloading::command_line%]",
|
||||
"filter": "[%key:ui::panel::config::server_control::section::reloading::filter%]",
|
||||
"statistics": "[%key:ui::panel::config::server_control::section::reloading::statistics%]",
|
||||
"generic": "[%key:ui::panel::config::server_control::section::reloading::generic%]",
|
||||
"generic_thermostat": "[%key:ui::panel::config::server_control::section::reloading::generic_thermostat%]",
|
||||
"homekit": "[%key:ui::panel::config::server_control::section::reloading::homekit%]",
|
||||
"min_max": "[%key:ui::panel::config::server_control::section::reloading::min_max%]",
|
||||
"history_stats": "[%key:ui::panel::config::server_control::section::reloading::history_stats%]",
|
||||
"trend": "[%key:ui::panel::config::server_control::section::reloading::trend%]",
|
||||
"ping": "[%key:ui::panel::config::server_control::section::reloading::ping%]",
|
||||
"filesize": "[%key:ui::panel::config::server_control::section::reloading::filesize%]",
|
||||
"telegram": "[%key:ui::panel::config::server_control::section::reloading::telegram%]",
|
||||
"smtp": "[%key:ui::panel::config::server_control::section::reloading::smtp%]",
|
||||
"mqtt": "[%key:ui::panel::config::server_control::section::reloading::mqtt%]",
|
||||
"rpi_gpio": "[%key:ui::panel::config::server_control::section::reloading::rpi_gpio%]",
|
||||
"themes": "[%key:ui::panel::config::server_control::section::reloading::themes%]"
|
||||
"reload": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::reload%]",
|
||||
"core": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::core%]",
|
||||
"group": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::group%]",
|
||||
"automation": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::automation%]",
|
||||
"script": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::script%]",
|
||||
"scene": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::scene%]",
|
||||
"person": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::person%]",
|
||||
"zone": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::zone%]",
|
||||
"input_boolean": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_boolean%]",
|
||||
"input_text": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_text%]",
|
||||
"input_number": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_number%]",
|
||||
"input_datetime": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_datetime%]",
|
||||
"input_select": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_select%]",
|
||||
"template": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::template%]",
|
||||
"universal": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::universal%]",
|
||||
"rest": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::rest%]",
|
||||
"command_line": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::command_line%]",
|
||||
"filter": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::filter%]",
|
||||
"statistics": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::statistics%]",
|
||||
"generic": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::generic%]",
|
||||
"generic_thermostat": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::generic_thermostat%]",
|
||||
"homekit": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::homekit%]",
|
||||
"min_max": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::min_max%]",
|
||||
"history_stats": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::history_stats%]",
|
||||
"trend": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::trend%]",
|
||||
"ping": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::ping%]",
|
||||
"filesize": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::filesize%]",
|
||||
"telegram": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::telegram%]",
|
||||
"smtp": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::smtp%]",
|
||||
"mqtt": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::mqtt%]",
|
||||
"rpi_gpio": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::rpi_gpio%]",
|
||||
"themes": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::themes%]"
|
||||
},
|
||||
"server_control": {
|
||||
"perform_action": "{action} server",
|
||||
"restart": "[%key:ui::panel::config::server_control::section::server_management::restart%]",
|
||||
"stop": "[%key:ui::panel::config::server_control::section::server_management::stop%]"
|
||||
"restart": "[%key:ui::panel::developer-tools::tabs::yaml::section::server_management::restart%]",
|
||||
"stop": "[%key:ui::panel::developer-tools::tabs::yaml::section::server_management::stop%]"
|
||||
},
|
||||
"types": {
|
||||
"reload": "Reload",
|
||||
@@ -686,12 +687,12 @@
|
||||
"users": "[%key:ui::panel::config::users::caption%]",
|
||||
"info": "[%key:ui::panel::config::info::caption%]",
|
||||
"blueprint": "[%key:ui::panel::config::blueprint::caption%]",
|
||||
"server_control": "[%key:ui::panel::config::server_control::caption%]"
|
||||
"server_control": "[%key:ui::panel::developer-tools::tabs::yaml::title%]"
|
||||
}
|
||||
},
|
||||
"filter_placeholder": "Entity Filter",
|
||||
"title": "Quick Search",
|
||||
"key_c_hint": "Press 'c' on any page to open this search bar",
|
||||
"key_c_hint": "Press 'c' on any page to open the search bar",
|
||||
"nothing_found": "Nothing found!"
|
||||
},
|
||||
"voice_command": {
|
||||
@@ -857,7 +858,7 @@
|
||||
"area_note": "By default the entities of a device are in the same area as the device. If you change the area of this entity, it will no longer follow the area of the device.",
|
||||
"follow_device_area": "Follow device area",
|
||||
"change_device_area": "Change device area",
|
||||
"configure_state": "Configure State"
|
||||
"configure_state": "{integration} options"
|
||||
}
|
||||
},
|
||||
"helper_settings": {
|
||||
@@ -994,6 +995,45 @@
|
||||
"recent_tx_messages": "{n} most recently transmitted message(s)",
|
||||
"show_as_yaml": "Show as YAML",
|
||||
"triggers": "Triggers"
|
||||
},
|
||||
"unsupported": {
|
||||
"title": "[%key:supervisor::system::supervisor::unsupported_title%]",
|
||||
"description": "[%key:supervisor::system::supervisor::unsupported_description%]",
|
||||
"reasons": {
|
||||
"apparmor": "[%key:supervisor::system::supervisor::unsupported_reason::apparmor%]",
|
||||
"content_trust": "[%key:supervisor::system::supervisor::unsupported_reason::content_trust%]",
|
||||
"dbus": "[%key:supervisor::system::supervisor::unsupported_reason::dbus%]",
|
||||
"docker_configuration": "[%key:supervisor::system::supervisor::unsupported_reason::docker_configuration%]",
|
||||
"docker_version": "[%key:supervisor::system::supervisor::unsupported_reason::docker_version%]",
|
||||
"job_conditions": "[%key:supervisor::system::supervisor::unsupported_reason::job_conditions%]",
|
||||
"lxc": "[%key:supervisor::system::supervisor::unsupported_reason::lxc%]",
|
||||
"network_manager": "[%key:supervisor::system::supervisor::unsupported_reason::network_manager%]",
|
||||
"os": "[%key:supervisor::system::supervisor::unsupported_reason::os%]",
|
||||
"os_agent": "[%key:supervisor::system::supervisor::unsupported_reason::os_agent%]",
|
||||
"privileged": "[%key:supervisor::system::supervisor::unsupported_reason::privileged%]",
|
||||
"software": "[%key:supervisor::system::supervisor::unsupported_reason::software%]",
|
||||
"source_mods": "[%key:supervisor::system::supervisor::unsupported_reason::source_mods%]",
|
||||
"systemd": "[%key:supervisor::system::supervisor::unsupported_reason::systemd%]",
|
||||
"systemd_resolved": "[%key:supervisor::system::supervisor::unsupported_reason::systemd_resolved%]"
|
||||
}
|
||||
},
|
||||
"unhealthy": {
|
||||
"title": "[%key:supervisor::system::supervisor::unhealthy_title%]",
|
||||
"description": "[%key:supervisor::system::supervisor::unhealthy_description%]",
|
||||
"reasons": {
|
||||
"privileged": "[%key:supervisor::system::supervisor::unhealthy_reason::privileged%]",
|
||||
"supervisor": "[%key:supervisor::system::supervisor::unhealthy_reason::supervisor%]",
|
||||
"setup": "[%key:supervisor::system::supervisor::unhealthy_reason::setup%]",
|
||||
"docker": "[%key:supervisor::system::supervisor::unhealthy_reason::docker%]",
|
||||
"untrusted": "[%key:supervisor::system::supervisor::unhealthy_reason::untrusted%]"
|
||||
}
|
||||
},
|
||||
"join_beta_channel": {
|
||||
"title": "Join the beta channel",
|
||||
"warning": "[%key:supervisor::system::supervisor::beta_warning%]",
|
||||
"backup": "[%key:supervisor::system::supervisor::beta_backup%]",
|
||||
"release_items": "[%key:supervisor::system::supervisor::beta_release_items%]",
|
||||
"confirm": "[%key:supervisor::system::supervisor::beta_join_confirm%]"
|
||||
}
|
||||
},
|
||||
"duration": {
|
||||
@@ -1044,7 +1084,8 @@
|
||||
"wrapping_up_startup": "Wrapping up startup, not everything will be available until it is finished.",
|
||||
"integration_starting": "Starting {integration}, not everything will be available until it is finished.",
|
||||
"triggered": "Triggered {name}",
|
||||
"dismiss": "Dismiss"
|
||||
"dismiss": "Dismiss",
|
||||
"no_matching_link_found": "No matching My link found for {path}"
|
||||
},
|
||||
"sidebar": {
|
||||
"external_app_configuration": "App Configuration",
|
||||
@@ -1071,7 +1112,7 @@
|
||||
},
|
||||
"automations": {
|
||||
"main": "Automations & Scenes",
|
||||
"secondary": "Manage automations, scenes, scripts and blueprints"
|
||||
"secondary": "Automations, scenes, scripts and blueprints"
|
||||
},
|
||||
"backup": {
|
||||
"main": "Backup",
|
||||
@@ -1079,11 +1120,11 @@
|
||||
},
|
||||
"supervisor": {
|
||||
"main": "Add-ons",
|
||||
"secondary": "Extend the function around Home Assistant"
|
||||
"secondary": "Run extra applications next to Home Assistant"
|
||||
},
|
||||
"dashboards": {
|
||||
"main": "Dashboards",
|
||||
"secondary": "Create customized sets of cards to control your home"
|
||||
"secondary": "Organize how you interact with your home"
|
||||
},
|
||||
"energy": {
|
||||
"main": "Energy",
|
||||
@@ -1091,15 +1132,15 @@
|
||||
},
|
||||
"tags": {
|
||||
"main": "Tags",
|
||||
"secondary": "Trigger automations when an NFC tag, QR code, etc. is scanned"
|
||||
"secondary": "Manage NFC tags and QR codes"
|
||||
},
|
||||
"people": {
|
||||
"main": "People",
|
||||
"secondary": "Manage the people that Home Assistant tracks"
|
||||
"secondary": "Manage who can access your home"
|
||||
},
|
||||
"areas": {
|
||||
"main": "Areas & Zones",
|
||||
"secondary": "Manage areas and zones that Home Assistant tracks"
|
||||
"secondary": "Manage locations in and around your house"
|
||||
},
|
||||
"companion": {
|
||||
"main": "Companion App",
|
||||
@@ -1137,6 +1178,8 @@
|
||||
"show": "show",
|
||||
"show_skipped": "Show skipped",
|
||||
"hide_skipped": "Hide skipped",
|
||||
"join_beta": "[%key:supervisor::system::supervisor::join_beta_action%]",
|
||||
"leave_beta": "[%key:supervisor::system::supervisor::leave_beta_action%]",
|
||||
"skipped": "Skipped"
|
||||
},
|
||||
"areas": {
|
||||
@@ -1195,6 +1238,9 @@
|
||||
"title": "Remove backup",
|
||||
"description": "Are you sure you want to remove the backup with the name {name}?",
|
||||
"confirm": "[%key:ui::common::remove%]"
|
||||
},
|
||||
"picker": {
|
||||
"search": "Search backups"
|
||||
}
|
||||
},
|
||||
"tag": {
|
||||
@@ -1449,7 +1495,9 @@
|
||||
"metric_example": "Celsius, kilograms",
|
||||
"find_currency_value": "Find your value",
|
||||
"save_button": "Save",
|
||||
"currency": "Currency"
|
||||
"currency": "Currency",
|
||||
"edit_location": "Edit location",
|
||||
"edit_location_description": "Location can be changed in zone settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1490,7 +1538,7 @@
|
||||
"copy_menu": "Copy menu",
|
||||
"copy_raw": "Raw Text",
|
||||
"copy_github": "For GitHub",
|
||||
"description": "Version, loaded integration and links to documentation",
|
||||
"description": "Version, loaded integrations and links to documentation",
|
||||
"home_assistant_logo": "Home Assistant logo",
|
||||
"path_configuration": "Path to configuration.yaml: {path}",
|
||||
"developed_by": "Developed by a bunch of awesome people.",
|
||||
@@ -1519,7 +1567,7 @@
|
||||
"search": "Search logs",
|
||||
"failed_get_logs": "Failed to get {provider} logs, {error}",
|
||||
"no_issues_search": "No issues found for search term ''{term}''",
|
||||
"load_full_log": "Load Full Home Assistant Log",
|
||||
"load_logs": "Load Full Logs",
|
||||
"loading_log": "Loading error log…",
|
||||
"no_errors": "No errors have been reported",
|
||||
"no_issues": "There are no new issues!",
|
||||
@@ -1536,7 +1584,8 @@
|
||||
"debug": "DEBUG"
|
||||
},
|
||||
"custom_integration": "custom integration",
|
||||
"error_from_custom_integration": "This error originated from a custom integration."
|
||||
"error_from_custom_integration": "This error originated from a custom integration.",
|
||||
"full_logs": "Full logs"
|
||||
},
|
||||
"lovelace": {
|
||||
"caption": "Dashboards",
|
||||
@@ -1618,65 +1667,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_control": {
|
||||
"caption": "Server Controls",
|
||||
"description": "Validate and restart the Home Assistant server",
|
||||
"section": {
|
||||
"validation": {
|
||||
"heading": "Configuration validation",
|
||||
"introduction": "Validate your configuration if you recently made some changes to your configuration and want to make sure that it is all valid.",
|
||||
"check_config": "Check configuration",
|
||||
"valid": "Configuration valid!",
|
||||
"invalid": "Configuration invalid"
|
||||
},
|
||||
"reloading": {
|
||||
"heading": "YAML configuration reloading",
|
||||
"introduction": "Some parts of Home Assistant can reload without requiring a restart. Clicking one of the options below will unload their current YAML configuration and load the new one.",
|
||||
"reload": "{domain}",
|
||||
"core": "Location & customizations",
|
||||
"group": "Groups, group entities, and notify services",
|
||||
"automation": "Automations",
|
||||
"script": "Scripts",
|
||||
"scene": "Scenes",
|
||||
"person": "People",
|
||||
"zone": "Zones",
|
||||
"input_boolean": "Input booleans",
|
||||
"input_button": "Input buttons",
|
||||
"input_text": "Input texts",
|
||||
"input_number": "Input numbers",
|
||||
"input_datetime": "Input date times",
|
||||
"input_select": "Input selects",
|
||||
"template": "Template entities",
|
||||
"universal": "Universal media player entities",
|
||||
"rest": "Rest entities and notify services",
|
||||
"command_line": "Command line entities",
|
||||
"filter": "Filter entities",
|
||||
"statistics": "Statistics entities",
|
||||
"generic": "Generic IP camera entities",
|
||||
"generic_thermostat": "Generic thermostat entities",
|
||||
"homekit": "HomeKit",
|
||||
"min_max": "Min/max entities",
|
||||
"history_stats": "History stats entities",
|
||||
"trend": "Trend entities",
|
||||
"ping": "Ping binary sensor entities",
|
||||
"filesize": "File size entities",
|
||||
"telegram": "Telegram notify services",
|
||||
"smtp": "SMTP notify services",
|
||||
"mqtt": "Manually configured MQTT entities",
|
||||
"rpi_gpio": "Raspberry Pi GPIO entities",
|
||||
"timer": "Timers",
|
||||
"themes": "Themes"
|
||||
},
|
||||
"server_management": {
|
||||
"heading": "Home Assistant",
|
||||
"introduction": "Restarting Home Assistant will stop your dashboard and automations. After the reboot, each configuration will be reloaded.",
|
||||
"restart": "Restart",
|
||||
"confirm_restart": "Are you sure you want to restart Home Assistant?",
|
||||
"stop": "Stop",
|
||||
"confirm_stop": "Are you sure you want to stop Home Assistant?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"automation": {
|
||||
"caption": "Automations",
|
||||
"description": "Create custom behavior rules for your home",
|
||||
@@ -2641,6 +2631,7 @@
|
||||
"create_zone": "Add Zone",
|
||||
"add_zone": "Add Zone",
|
||||
"edit_zone": "Edit Zone",
|
||||
"edit_home": "Edit Home",
|
||||
"confirm_delete": "Are you sure you want to delete this zone?",
|
||||
"can_not_edit": "Unable to edit zone",
|
||||
"configured_in_yaml": "Zones configured via configuration.yaml cannot be edited via the UI.",
|
||||
@@ -3167,10 +3158,14 @@
|
||||
},
|
||||
"system_health": {
|
||||
"caption": "System Health",
|
||||
"cpu_usage": "CPU Usage",
|
||||
"ram_usage": "RAM Usage",
|
||||
"cpu_usage": "Process Usage",
|
||||
"ram_usage": "Memory Usage",
|
||||
"core_stats": "Core Stats",
|
||||
"supervisor_stats": "Supervisor Stats"
|
||||
},
|
||||
"system_dashboard": {
|
||||
"confirm_restart": "Are you sure you want to restart Home Assistant?",
|
||||
"restart_homeassistant": "Restart Home Assistant"
|
||||
}
|
||||
},
|
||||
"lovelace": {
|
||||
@@ -4150,6 +4145,65 @@
|
||||
}
|
||||
},
|
||||
"adjust_sum": "Adjust sum"
|
||||
},
|
||||
"yaml": {
|
||||
"title": "YAML Configuration",
|
||||
"section": {
|
||||
"validation": {
|
||||
"heading": "Configuration validation",
|
||||
"introduction": "Validate your configuration if you recently made some changes to it and want to make sure that it is all valid.",
|
||||
"check_config": "Check configuration",
|
||||
"valid": "Configuration valid!",
|
||||
"invalid": "Configuration invalid!"
|
||||
},
|
||||
"reloading": {
|
||||
"heading": "YAML configuration reloading",
|
||||
"introduction": "Some parts of Home Assistant can reload without requiring a restart. Clicking one of the options below will unload their current YAML configuration and load the new one.",
|
||||
"reload": "{domain}",
|
||||
"core": "Location & customizations",
|
||||
"group": "Groups, group entities, and notify services",
|
||||
"automation": "Automations",
|
||||
"script": "Scripts",
|
||||
"scene": "Scenes",
|
||||
"person": "People",
|
||||
"zone": "Zones",
|
||||
"input_boolean": "Input booleans",
|
||||
"input_button": "Input buttons",
|
||||
"input_text": "Input texts",
|
||||
"input_number": "Input numbers",
|
||||
"input_datetime": "Input date times",
|
||||
"input_select": "Input selects",
|
||||
"template": "Template entities",
|
||||
"universal": "Universal media player entities",
|
||||
"rest": "Rest entities and notify services",
|
||||
"command_line": "Command line entities",
|
||||
"filter": "Filter entities",
|
||||
"statistics": "Statistics entities",
|
||||
"generic": "Generic IP camera entities",
|
||||
"generic_thermostat": "Generic thermostat entities",
|
||||
"homekit": "HomeKit",
|
||||
"min_max": "Min/max entities",
|
||||
"history_stats": "History stats entities",
|
||||
"trend": "Trend entities",
|
||||
"ping": "Ping binary sensor entities",
|
||||
"filesize": "File size entities",
|
||||
"telegram": "Telegram notify services",
|
||||
"smtp": "SMTP notify services",
|
||||
"mqtt": "Manually configured MQTT entities",
|
||||
"rpi_gpio": "Raspberry Pi GPIO entities",
|
||||
"timer": "Timers",
|
||||
"themes": "Themes"
|
||||
},
|
||||
"server_management": {
|
||||
"heading": "Home Assistant",
|
||||
"introduction": "Restarting Home Assistant will stop your dashboard and automations. After the reboot, each configuration will be reloaded.",
|
||||
"restart": "Restart",
|
||||
"restart_home_assistant": "Restart Home Assistant",
|
||||
"confirm_restart": "Are you sure you want to restart Home Assistant?",
|
||||
"stop": "Stop",
|
||||
"confirm_stop": "Are you sure you want to stop Home Assistant?"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4236,7 +4290,8 @@
|
||||
},
|
||||
"tips": {
|
||||
"key_c_hint": "Press 'c' on any page to open the command dialog",
|
||||
"key_e_hint": "Press 'e' on any page to open the entity search dialog"
|
||||
"key_e_hint": "Press 'e' on any page to open the entity search dialog",
|
||||
"key_m_hint": "Press 'm' on any page to get the My Home Assistant link"
|
||||
}
|
||||
},
|
||||
"supervisor": {
|
||||
|
34
test/common/datetime/duration.ts
Normal file
34
test/common/datetime/duration.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { assert } from "chai";
|
||||
|
||||
import { formatDuration } from "../../../src/common/datetime/duration";
|
||||
|
||||
describe("formatDuration", () => {
|
||||
it("works", () => {
|
||||
assert.strictEqual(formatDuration("0", "s"), "0");
|
||||
assert.strictEqual(formatDuration("65", "s"), "1:05");
|
||||
assert.strictEqual(formatDuration("3665", "s"), "1:01:05");
|
||||
assert.strictEqual(formatDuration("39665", "s"), "11:01:05");
|
||||
assert.strictEqual(formatDuration("932093", "s"), "258:54:53");
|
||||
|
||||
assert.strictEqual(formatDuration("0", "min"), "0");
|
||||
assert.strictEqual(formatDuration("65", "min"), "1:05:00");
|
||||
assert.strictEqual(formatDuration("3665", "min"), "61:05:00");
|
||||
assert.strictEqual(formatDuration("39665", "min"), "661:05:00");
|
||||
assert.strictEqual(formatDuration("932093", "min"), "15534:53:00");
|
||||
assert.strictEqual(formatDuration("12.4", "min"), "12:24");
|
||||
|
||||
assert.strictEqual(formatDuration("0", "h"), "0");
|
||||
assert.strictEqual(formatDuration("65", "h"), "65:00:00");
|
||||
assert.strictEqual(formatDuration("3665", "h"), "3665:00:00");
|
||||
assert.strictEqual(formatDuration("39665", "h"), "39665:00:00");
|
||||
assert.strictEqual(formatDuration("932093", "h"), "932093:00:00");
|
||||
assert.strictEqual(formatDuration("24.3", "h"), "24:18:00");
|
||||
assert.strictEqual(formatDuration("24.32423", "h"), "24:19:27");
|
||||
|
||||
assert.strictEqual(formatDuration("0", "d"), "0");
|
||||
assert.strictEqual(formatDuration("65", "d"), "1560:00:00");
|
||||
assert.strictEqual(formatDuration("3665", "d"), "87960:00:00");
|
||||
assert.strictEqual(formatDuration("39665", "d"), "951960:00:00");
|
||||
assert.strictEqual(formatDuration("932093", "d"), "22370232:00:00");
|
||||
});
|
||||
});
|
@@ -8,5 +8,6 @@ describe("secondsToDuration", () => {
|
||||
assert.strictEqual(secondsToDuration(65), "1:05");
|
||||
assert.strictEqual(secondsToDuration(3665), "1:01:05");
|
||||
assert.strictEqual(secondsToDuration(39665), "11:01:05");
|
||||
assert.strictEqual(secondsToDuration(932093), "258:54:53");
|
||||
});
|
||||
});
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import { assert } from "chai";
|
||||
import { assert, expect } from "chai";
|
||||
|
||||
import {
|
||||
fuzzyFilterSort,
|
||||
fuzzySequentialMatch,
|
||||
fuzzySortFilterSort,
|
||||
ScorableTextItem,
|
||||
} from "../../../src/common/string/filter/sequence-matching";
|
||||
|
||||
@@ -11,45 +10,34 @@ describe("fuzzySequentialMatch", () => {
|
||||
strings: ["automation.ticker", "Stocks"],
|
||||
};
|
||||
|
||||
const createExpectation: (
|
||||
pattern,
|
||||
expected
|
||||
) => {
|
||||
pattern: string;
|
||||
expected: string | number | undefined;
|
||||
} = (pattern, expected) => ({
|
||||
pattern,
|
||||
expected,
|
||||
});
|
||||
|
||||
const shouldMatchEntity = [
|
||||
createExpectation("automation.ticker", 131),
|
||||
createExpectation("automation.ticke", 121),
|
||||
createExpectation("automation.", 82),
|
||||
createExpectation("au", 10),
|
||||
createExpectation("automationticker", 85),
|
||||
createExpectation("tion.tick", 8),
|
||||
createExpectation("ticker", -4),
|
||||
createExpectation("automation.r", 73),
|
||||
createExpectation("tick", -8),
|
||||
createExpectation("aumatick", 9),
|
||||
createExpectation("aion.tck", 4),
|
||||
createExpectation("ioticker", -4),
|
||||
createExpectation("atmto.ikr", -34),
|
||||
createExpectation("uoaintce", -39),
|
||||
createExpectation("au.tce", -3),
|
||||
createExpectation("tomaontkr", -19),
|
||||
createExpectation("s", 1),
|
||||
createExpectation("stocks", 42),
|
||||
createExpectation("sks", -5),
|
||||
"",
|
||||
" ",
|
||||
"automation.ticker",
|
||||
"stocks",
|
||||
"automation.ticke",
|
||||
"automation. ticke",
|
||||
"automation.",
|
||||
"automationticker",
|
||||
"automation.r",
|
||||
"aumatick",
|
||||
"tion.tick",
|
||||
"aion.tck",
|
||||
"s",
|
||||
"au.tce",
|
||||
"au",
|
||||
"ticker",
|
||||
"tick",
|
||||
"ioticker",
|
||||
"sks",
|
||||
"tomaontkr",
|
||||
"atmto.ikr",
|
||||
"uoaintce",
|
||||
];
|
||||
|
||||
const shouldNotMatchEntity = [
|
||||
"",
|
||||
" ",
|
||||
"abcdefghijklmnopqrstuvwxyz",
|
||||
"automation.tickerz",
|
||||
"automation. ticke",
|
||||
"1",
|
||||
"noitamotua",
|
||||
"autostocks",
|
||||
@@ -57,23 +45,23 @@ describe("fuzzySequentialMatch", () => {
|
||||
];
|
||||
|
||||
describe(`Entity '${item.strings[0]}'`, () => {
|
||||
for (const expectation of shouldMatchEntity) {
|
||||
it(`matches '${expectation.pattern}' with return of '${expectation.expected}'`, () => {
|
||||
const res = fuzzySequentialMatch(expectation.pattern, item);
|
||||
assert.equal(res, expectation.expected);
|
||||
for (const filter of shouldMatchEntity) {
|
||||
it(`Should matches ${filter}`, () => {
|
||||
const res = fuzzySortFilterSort(filter, [item]);
|
||||
assert.lengthOf(res, 1);
|
||||
});
|
||||
}
|
||||
|
||||
for (const badFilter of shouldNotMatchEntity) {
|
||||
it(`fails to match with '${badFilter}'`, () => {
|
||||
const res = fuzzySequentialMatch(badFilter, item);
|
||||
assert.equal(res, undefined);
|
||||
const res = fuzzySortFilterSort(badFilter, [item]);
|
||||
assert.lengthOf(res, 0);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("fuzzyFilterSort", () => {
|
||||
describe("fuzzyFilterSort original tests", () => {
|
||||
const filter = "ticker";
|
||||
const automationTicker = {
|
||||
strings: ["automation.ticker", "Stocks"],
|
||||
@@ -105,14 +93,137 @@ describe("fuzzyFilterSort", () => {
|
||||
|
||||
it(`filters and sorts correctly`, () => {
|
||||
const expectedItemsAfterFilter = [
|
||||
{ ...ticker, score: 44 },
|
||||
{ ...sensorTicker, score: 1 },
|
||||
{ ...automationTicker, score: -4 },
|
||||
{ ...timerCheckRouter, score: -8 },
|
||||
{ ...ticker, score: 0 },
|
||||
{ ...sensorTicker, score: -14 },
|
||||
{ ...automationTicker, score: -22 },
|
||||
{ ...timerCheckRouter, score: -32012 },
|
||||
];
|
||||
|
||||
const res = fuzzyFilterSort(filter, itemsBeforeFilter);
|
||||
const res = fuzzySortFilterSort(filter, itemsBeforeFilter);
|
||||
|
||||
assert.deepEqual(res, expectedItemsAfterFilter);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Fuzzy filter new tests", () => {
|
||||
const testEntities = [
|
||||
{
|
||||
id: "binary_sensor.garage_door_opened",
|
||||
name: "Garage Door Opened (Sensor, Binary)",
|
||||
},
|
||||
{
|
||||
id: "sensor.garage_door_status",
|
||||
name: "Garage Door Opened (Sensor)",
|
||||
},
|
||||
{
|
||||
id: "sensor.temperature_living_room",
|
||||
name: "[Living room] temperature",
|
||||
},
|
||||
{
|
||||
id: "sensor.temperature_parents_bedroom",
|
||||
name: "[Parents bedroom] temperature",
|
||||
},
|
||||
{
|
||||
id: "sensor.temperature_children_bedroom",
|
||||
name: "[Children bedroom] temperature",
|
||||
},
|
||||
];
|
||||
|
||||
function testEntitySearch(
|
||||
searchInput: string | null,
|
||||
expectedResults: string[]
|
||||
) {
|
||||
const sortableEntities = testEntities.map((entity) => ({
|
||||
strings: [entity.id, entity.name],
|
||||
entity: entity,
|
||||
}));
|
||||
const sortedEntities = fuzzySortFilterSort(
|
||||
searchInput || "",
|
||||
sortableEntities
|
||||
);
|
||||
// console.log(sortedEntities);
|
||||
expect(sortedEntities.map((it) => it.entity.id)).to.have.ordered.members(
|
||||
expectedResults
|
||||
);
|
||||
}
|
||||
|
||||
it(`test empty or null query`, () => {
|
||||
testEntitySearch(
|
||||
"",
|
||||
testEntities.map((it) => it.id)
|
||||
);
|
||||
testEntitySearch(
|
||||
null,
|
||||
testEntities.map((it) => it.id)
|
||||
);
|
||||
});
|
||||
|
||||
it(`test single word search`, () => {
|
||||
testEntitySearch("bedroom", [
|
||||
"sensor.temperature_parents_bedroom",
|
||||
"sensor.temperature_children_bedroom",
|
||||
]);
|
||||
});
|
||||
|
||||
it(`test no result`, () => {
|
||||
testEntitySearch("does not exist", []);
|
||||
testEntitySearch("betroom", []);
|
||||
});
|
||||
|
||||
it(`test single word search with typo`, () => {
|
||||
testEntitySearch("bedorom", [
|
||||
"sensor.temperature_parents_bedroom",
|
||||
"sensor.temperature_children_bedroom",
|
||||
]);
|
||||
});
|
||||
|
||||
it(`test multi word search`, () => {
|
||||
testEntitySearch("bedroom children", [
|
||||
"sensor.temperature_children_bedroom",
|
||||
]);
|
||||
});
|
||||
|
||||
it(`test partial word search`, () => {
|
||||
testEntitySearch("room", [
|
||||
"sensor.temperature_living_room",
|
||||
"sensor.temperature_parents_bedroom",
|
||||
"sensor.temperature_children_bedroom",
|
||||
]);
|
||||
});
|
||||
|
||||
it(`test mixed cased word search`, () => {
|
||||
testEntitySearch("garage binary", ["binary_sensor.garage_door_opened"]);
|
||||
});
|
||||
|
||||
it(`test mixed id and name search`, () => {
|
||||
testEntitySearch("status opened", ["sensor.garage_door_status"]);
|
||||
});
|
||||
|
||||
it(`test special chars in query`, () => {
|
||||
testEntitySearch("sensor.temperature", [
|
||||
"sensor.temperature_living_room",
|
||||
"sensor.temperature_parents_bedroom",
|
||||
"sensor.temperature_children_bedroom",
|
||||
]);
|
||||
|
||||
testEntitySearch("sensor.temperature parents", [
|
||||
"sensor.temperature_parents_bedroom",
|
||||
]);
|
||||
testEntitySearch("parents_Bedroom", ["sensor.temperature_parents_bedroom"]);
|
||||
});
|
||||
|
||||
it(`test search in name`, () => {
|
||||
testEntitySearch("Binary)", ["binary_sensor.garage_door_opened"]);
|
||||
|
||||
testEntitySearch("Binary)NotExists", []);
|
||||
});
|
||||
|
||||
it(`test regex special chars`, () => {
|
||||
// Should return an empty result, but no error
|
||||
testEntitySearch("\\{}()*+?.,[])", []);
|
||||
|
||||
testEntitySearch("[Children bedroom]", [
|
||||
"sensor.temperature_children_bedroom",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@@ -8433,6 +8433,13 @@ fsevents@^1.2.7:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fuzzysort@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "fuzzysort@npm:1.2.1"
|
||||
checksum: 74dad902a0aef6c3237d5ae5330aacca23d408f0e07125fcc39b57561b4c29da512fbf3826c3f3918da89f132f5b393cf5d56b3217282ecfb80a90124bdf03d1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gauge@npm:~2.7.3":
|
||||
version: 2.7.4
|
||||
resolution: "gauge@npm:2.7.4"
|
||||
@@ -9119,6 +9126,7 @@ fsevents@^1.2.7:
|
||||
fancy-log: ^1.3.3
|
||||
fs-extra: ^7.0.1
|
||||
fuse.js: ^6.0.0
|
||||
fuzzysort: ^1.2.1
|
||||
glob: ^7.2.0
|
||||
google-timezones-json: ^1.0.2
|
||||
gulp: ^4.0.2
|
||||
|
Reference in New Issue
Block a user