Compare commits

..

1 Commits

Author SHA1 Message Date
Zack
c363e1a2db Make sure config exists 2022-04-22 10:33:27 -05:00
111 changed files with 3312 additions and 5406 deletions

View File

@@ -3,10 +3,10 @@ const webpack = require("webpack");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const log = require("fancy-log");
const WebpackBar = require("webpackbar");
const paths = require("./paths.js");
const bundle = require("./bundle.js");
const log = require("fancy-log");
const WebpackBar = require("webpackbar");
class LogStartCompilePlugin {
ignoredFirst = false;
@@ -138,8 +138,6 @@ const createWebpackConfig = ({
"lit/directives/cache$": "lit/directives/cache.js",
"lit/directives/repeat$": "lit/directives/repeat.js",
"lit/polyfill-support$": "lit/polyfill-support.js",
"@lit-labs/virtualizer/layouts/grid":
"@lit-labs/virtualizer/layouts/grid.js",
},
},
output: {

View File

@@ -8,7 +8,7 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import type { ConditionWithShorthand } from "../../../../src/data/automation";
import type { Condition } from "../../../../src/data/automation";
import "../../../../src/panels/config/automation/condition/ha-automation-condition";
import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
import { HaLogicalCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-logical";
@@ -20,7 +20,7 @@ import { HaTimeCondition } from "../../../../src/panels/config/automation/condit
import { HaTriggerCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
import { HaZoneCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
const SCHEMAS: { name: string; conditions: Condition[] }[] = [
{
name: "State",
conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }],
@@ -69,14 +69,6 @@ const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
name: "Trigger",
conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }],
},
{
name: "Shorthand",
conditions: [
{ and: HaLogicalCondition.defaultConfig.conditions },
{ or: HaLogicalCondition.defaultConfig.conditions },
{ not: HaLogicalCondition.defaultConfig.conditions },
],
},
];
@customElement("demo-automation-editor-condition")

View File

@@ -2,7 +2,6 @@ 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,
@@ -12,6 +11,7 @@ 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`<ha-ansi-to-html
? html`<hassio-ansi-to-html
.content=${this._content}
></ha-ansi-to-html>`
></hassio-ansi-to-html>`
: ""}
</div>
<div class="card-actions">

View File

@@ -1,7 +1,7 @@
import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
import { mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
import {
css,
CSSResultGroup,
@@ -166,15 +166,7 @@ export class HassioBackups extends LitElement {
}
return html`
<hass-tabs-subpage-data-table
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
? [
{
translationKey: "panel.backups",
path: `/hassio/backups`,
iconPath: mdiBackupRestore,
},
]
: supervisorTabs(this.hass)}
.tabs=${supervisorTabs(this.hass)}
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.searchLabel=${this.supervisor.localize("search")}
@@ -190,9 +182,7 @@ export class HassioBackups extends LitElement {
selectable
hasFab
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
back-path=${atLeastVersion(this.hass.config.version, 2022, 5)
? "/config/system"
: "/config"}
back-path="/config"
supervisor
>
<ha-button-menu

View File

@@ -10,8 +10,8 @@ interface State {
backgroundColor: null | string;
}
@customElement("ha-ansi-to-html")
class HaAnsiToHtml extends LitElement {
@customElement("hassio-ansi-to-html")
class HassioAnsiToHtml extends LitElement {
@property() public content!: string;
protected render(): TemplateResult | void {
@@ -241,6 +241,6 @@ class HaAnsiToHtml extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-ansi-to-html": HaAnsiToHtml;
"hassio-ansi-to-html": HassioAnsiToHtml;
}
}

View File

@@ -10,7 +10,6 @@ import { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addons";
import "./hassio-update";
import "../../../src/layouts/hass-subpage";
@customElement("hassio-dashboard")
class HassioDashboard extends LitElement {
@@ -23,31 +22,6 @@ class HassioDashboard extends LitElement {
@property({ attribute: false }) public route!: Route;
protected render(): TemplateResult {
if (atLeastVersion(this.hass.config.version, 2022, 5)) {
return html`<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.header=${this.supervisor.localize("panel.addons")}
>
<hassio-addons
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-addons>
<a href="/hassio/store">
<ha-fab
.label=${this.supervisor.localize("panel.store")}
extended
class="non-tabs"
>
<ha-svg-icon
slot="icon"
.path=${mdiStorePlus}
></ha-svg-icon> </ha-fab
></a>
</hass-subpage>`;
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
@@ -100,12 +74,6 @@ class HassioDashboard extends LitElement {
.content {
margin: 0 auto;
}
ha-fab.non-tabs {
position: fixed;
right: calc(16px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
}
`,
];
}

View File

@@ -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 { mainWindow } from "../../src/common/dom/get_main_window";
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
import { mainWindow } from "../../src/common/dom/get_main_window";
import { navigate } from "../../src/common/navigate";
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor";
@@ -73,14 +73,6 @@ 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!);
}

View File

@@ -15,7 +15,7 @@ import {
} from "../../src/panels/my/ha-panel-my";
import { HomeAssistant, Route } from "../../src/types";
export const REDIRECTS: Redirects = {
const REDIRECTS: Redirects = {
supervisor: {
redirect: "/hassio/dashboard",
},

View File

@@ -8,27 +8,24 @@ import { atLeastVersion } from "../../src/common/config/version";
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../src/types";
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] =>
atLeastVersion(hass.config.version, 2022, 5)
? []
: [
{
translationKey: atLeastVersion(hass.config.version, 2021, 12)
? "panel.addons"
: "panel.dashboard",
path: `/hassio/dashboard`,
iconPath: atLeastVersion(hass.config.version, 2021, 12)
? mdiPuzzle
: mdiViewDashboard,
},
{
translationKey: "panel.backups",
path: `/hassio/backups`,
iconPath: mdiBackupRestore,
},
{
translationKey: "panel.system",
path: `/hassio/system`,
iconPath: mdiCogs,
},
];
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] => [
{
translationKey: atLeastVersion(hass.config.version, 2021, 12)
? "panel.addons"
: "panel.dashboard",
path: `/hassio/dashboard`,
iconPath: atLeastVersion(hass.config.version, 2021, 12)
? mdiPuzzle
: mdiViewDashboard,
},
{
translationKey: "panel.backups",
path: `/hassio/backups`,
iconPath: mdiBackupRestore,
},
{
translationKey: "panel.system",
path: `/hassio/system`,
iconPath: mdiCogs,
},
];

View File

@@ -23,10 +23,6 @@ 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";
@@ -34,6 +30,11 @@ 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;

View File

@@ -1,4 +1,3 @@
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";
@@ -12,6 +11,7 @@ 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`<ha-ansi-to-html .content=${this._content}>
</ha-ansi-to-html>`
? html`<hassio-ansi-to-html .content=${this._content}>
</hassio-ansi-to-html>`
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
</div>
<div class="card-actions">

View File

@@ -106,7 +106,6 @@
"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",

View File

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

View File

@@ -1,16 +0,0 @@
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";

View File

@@ -13,7 +13,6 @@ 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,
@@ -29,21 +28,6 @@ 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, {

View File

@@ -0,0 +1,244 @@
// 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,
}

View File

@@ -0,0 +1,551 @@
/* 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)
);
}

View File

@@ -1,4 +1,52 @@
import fuzzysort from "fuzzysort";
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;
};
/**
* An interface that objects must extend in order to use the fuzzy sequence matcher
@@ -18,48 +66,18 @@ export interface ScorableTextItem {
strings: string[];
}
export type FuzzyFilterSort = <T extends ScorableTextItem>(
type FuzzyFilterSort = <T extends ScorableTextItem>(
filter: string,
items: T[]
) => T[];
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
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) =>
items
.map((item) => {
item.score = scorer(item.strings);
item.score = fuzzySequentialMatch(filter, item);
return item;
})
.filter((item) => item.score !== undefined && item.score > -100000)
.filter((item) => item.score !== undefined)
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
);
};
export const defaultFuzzyFilterSort = fuzzySortFilterSort;

View File

@@ -1,4 +1,4 @@
export const promiseTimeout = (ms: number, promise: Promise<any> | any) => {
export const promiseTimeout = (ms: number, promise: Promise<any>) => {
const timeout = new Promise((_resolve, reject) => {
setTimeout(() => {
reject(`Timed out in ${ms} ms.`);

View File

@@ -1,18 +0,0 @@
import { HomeAssistant } from "../../types";
export const subscribePollingCollection = (
hass: HomeAssistant,
updateData: (hass: HomeAssistant) => void,
interval: number
) => {
let timeout;
const fetchData = async () => {
try {
await updateData(hass);
} finally {
timeout = setTimeout(() => fetchData(), interval);
}
};
fetchData();
return () => clearTimeout(timeout);
};

View File

@@ -1,167 +1,165 @@
export const currencies = [
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTN",
"BWP",
"BYN",
"BYR",
"BZD",
"CAD",
"CDF",
"CHF",
"CLP",
"CNY",
"COP",
"CRC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"INR",
"IQD",
"IRR",
"ISK",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LTL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRO",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"SSP",
"STD",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VND",
"VUV",
"WST",
"XAF",
"XCD",
"XOF",
"XPF",
"YER",
"ZAR",
"ZMK",
"ZWL",
];
export const createCurrencyListEl = () => {
const list = document.createElement("datalist");
list.id = "currencies";
for (const currency of currencies) {
for (const currency of [
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTN",
"BWP",
"BYN",
"BYR",
"BZD",
"CAD",
"CDF",
"CHF",
"CLP",
"CNY",
"COP",
"CRC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"INR",
"IQD",
"IRR",
"ISK",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LTL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRO",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"SSP",
"STD",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VND",
"VUV",
"WST",
"XAF",
"XCD",
"XOF",
"XPF",
"YER",
"ZAR",
"ZMK",
"ZWL",
]) {
const option = document.createElement("option");
option.value = currency;
option.innerHTML = currency;

View File

@@ -7,26 +7,25 @@ import type {
SortableColumnContainer,
SortingDirection,
} from "./ha-data-table";
import { fuzzyMatcher } from "../../common/string/filter/sequence-matching";
const filterData = (
data: DataTableRowData[],
columns: SortableColumnContainer,
filter: string
) => {
const matcher = fuzzyMatcher(filter);
filter = filter.toUpperCase();
return data.filter((row) =>
Object.entries(columns).some((columnEntry) => {
const [key, column] = columnEntry;
if (column.filterable) {
if (
matcher(
String(
column.filterKey
? row[column.valueColumn || key][column.filterKey]
: row[column.valueColumn || key]
)
String(
column.filterKey
? row[column.valueColumn || key][column.filterKey]
: row[column.valueColumn || key]
)
.toUpperCase()
.includes(filter)
) {
return true;
}

View File

@@ -198,10 +198,9 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
this.hass,
deviceEntityLookup[device.id]
),
area:
device.area_id && areaLookup[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].name
: this.hass.localize("ui.components.device-picker.no_area"),
}));
if (!outputDevices.length) {
return [

View File

@@ -15,7 +15,6 @@ 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;
@@ -337,18 +336,11 @@ export class HaEntityPicker extends LitElement {
}
private _filterChanged(ev: CustomEvent): void {
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
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)
);
}

View File

@@ -409,7 +409,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
name,
});
this._areas = [...this._areas!, area];
(this.comboBox as any).filteredItems = this._getAreas(
(this.comboBox as any).items = this._getAreas(
this._areas!,
this._devices!,
this._entities!,

View File

@@ -310,7 +310,6 @@ export class HaBaseTimeInput extends LitElement {
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
overflow: hidden;
position: relative;
direction: ltr;
}
ha-textfield {
width: 40px;

View File

@@ -1,79 +0,0 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { roundWithOneDecimal } from "../util/calculate";
import "./ha-bar";
import "./ha-settings-row";
@customElement("ha-metric")
class HaMetric extends LitElement {
@property({ type: Number }) public value!: number;
@property({ type: String }) public heading!: string;
@property({ type: String }) public tooltip?: string;
protected render(): TemplateResult {
const roundedValue = roundWithOneDecimal(this.value);
return html`
<ha-settings-row>
<span slot="heading"> ${this.heading} </span>
<div slot="description" .title=${this.tooltip ?? ""}>
<span class="value"> ${roundedValue} % </span>
<ha-bar
class=${classMap({
"target-warning": roundedValue > 50,
"target-critical": roundedValue > 85,
})}
.value=${this.value}
></ha-bar>
</div>
</ha-settings-row>
`;
}
static get styles(): CSSResultGroup {
return css`
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-settings-row > div[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
display: flex;
justify-content: space-between;
}
ha-bar {
--ha-bar-primary-color: var(
--metric-bar-ok-color,
var(--success-color)
);
}
.target-warning {
--ha-bar-primary-color: var(
--metric-bar-warning-color,
var(--warning-color)
);
}
.target-critical {
--ha-bar-primary-color: var(
--metric-bar-critical-color,
var(--error-color)
);
}
.value {
width: 48px;
padding-right: 4px;
flex-shrink: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-metric": HaMetric;
}
}

View File

@@ -4,9 +4,9 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import type { HomeAssistant } from "../types";
import "./ha-clickable-list-item";
import "./ha-icon-next";
import "./ha-svg-icon";
import "./ha-clickable-list-item";
@customElement("ha-navigation-list")
class HaNavigationList extends LitElement {
@@ -56,9 +56,6 @@ class HaNavigationList extends LitElement {
}
static styles: CSSResultGroup = css`
:host {
--mdc-list-vertical-padding: 0;
}
a {
text-decoration: none;
color: var(--primary-text-color);
@@ -71,7 +68,6 @@ class HaNavigationList extends LitElement {
color: var(--secondary-text-color);
height: 24px;
width: 24px;
display: block;
}
ha-svg-icon {
padding: 8px;
@@ -82,10 +78,9 @@ class HaNavigationList extends LitElement {
.icon-background ha-svg-icon {
color: #fff;
}
ha-clickable-list-item {
mwc-list-item {
cursor: pointer;
font-size: var(--navigation-list-item-title-font-size);
padding: var(--navigation-list-item-padding) 0;
}
`;
}

View File

@@ -163,9 +163,6 @@ 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"],

View File

@@ -1,4 +1,4 @@
import { css, html, LitElement } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
@@ -76,13 +76,6 @@ export class HaLocationSelector extends LitElement {
const radius = ev.detail.radius;
fireEvent(this, "value-changed", { value: { ...this.value, radius } });
}
static styles = css`
:host {
display: block;
height: 400px;
}
`;
}
declare global {

View File

@@ -107,7 +107,6 @@ export class HaNumberSelector extends LitElement {
display: flex;
justify-content: space-between;
align-items: center;
direction: ltr;
}
ha-slider {
flex: 1;

View File

@@ -472,7 +472,6 @@ 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)

View File

@@ -47,7 +47,7 @@ export class HaSettingsRow extends LitElement {
display: contents;
}
:host(:not([narrow])) .content {
display: var(--settings-row-content-display, flex);
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: var(--settings-row-prefix-display);
display: contents;
}
:host([narrow]) .prefix-wrap {
display: flex;

View File

@@ -1051,6 +1051,9 @@ 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;

View File

@@ -151,7 +151,6 @@ class DialogMediaPlayerBrowse extends LitElement {
ha-media-player-browse {
--media-browser-max-height: calc(100vh - 65px);
height: calc(100vh - 65px);
}
@media (min-width: 800px) {
@@ -164,7 +163,6 @@ class DialogMediaPlayerBrowse extends LitElement {
ha-media-player-browse {
position: initial;
--media-browser-max-height: 100vh - 137px;
height: 100vh - 137px;
width: 700px;
}
}

View File

@@ -3,8 +3,6 @@ import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import { grid } from "@lit-labs/virtualizer/layouts/grid";
import "@lit-labs/virtualizer";
import {
css,
CSSResultGroup,
@@ -18,13 +16,16 @@ import {
eventOptions,
property,
query,
queryAll,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { until } from "lit/directives/until";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce";
import { getSignedPath } from "../../data/auth";
import type { MediaPlayerItem } from "../../data/media-player";
import {
browseMediaPlayer,
@@ -39,18 +40,18 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
import { documentationUrl } from "../../util/documentation-url";
import "../entity/ha-entity-picker";
import "../ha-button-menu";
import "../ha-card";
import type { HaCard } from "../ha-card";
import "../ha-circular-progress";
import "../ha-fab";
import "../ha-icon-button";
import "../ha-svg-icon";
import "./ha-browse-media-tts";
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
import { getSignedPath } from "../../data/auth";
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
declare global {
interface HASSDomEvents {
@@ -100,6 +101,8 @@ export class HaMediaPlayerBrowse extends LitElement {
@query(".content") private _content?: HTMLDivElement;
@queryAll(".lazythumbnail") private _thumbnails?: HaCard[];
private _headerOffsetHeight = 0;
private _resizeObserver?: ResizeObserver;
@@ -145,6 +148,326 @@ export class HaMediaPlayerBrowse extends LitElement {
}
}
protected render(): TemplateResult {
if (this._error) {
return html`
<div class="container">${this._renderError(this._error)}</div>
`;
}
if (!this._currentItem) {
return html`<ha-circular-progress active></ha-circular-progress>`;
}
const currentItem = this._currentItem;
const subtitle = this.hass.localize(
`ui.components.media-browser.class.${currentItem.media_class}`
);
const children = currentItem.children || [];
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
const childrenMediaClass = currentItem.children_media_class
? MediaClassBrowserSettings[currentItem.children_media_class]
: MediaClassBrowserSettings.directory;
return html`
${
currentItem.can_play
? html` <div
class="header ${classMap({
"no-img": !currentItem.thumbnail,
"no-dialog": !this.dialog,
})}"
@transitionend=${this._setHeaderHeight}
>
<div class="header-content">
${currentItem.thumbnail
? html`
<div
class="img"
style=${styleMap({
backgroundImage: currentItem.thumbnail
? `url(${currentItem.thumbnail})`
: "none",
})}
>
${this._narrow && currentItem?.can_play
? html`
<ha-fab
mini
.item=${currentItem}
@click=${this._actionClicked}
>
<ha-svg-icon
slot="icon"
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
></ha-svg-icon>
${this.hass.localize(
`ui.components.media-browser.${this.action}`
)}
</ha-fab>
`
: ""}
</div>
`
: html``}
<div class="header-info">
<div class="breadcrumb">
<h1 class="title">${currentItem.title}</h1>
${subtitle
? html` <h2 class="subtitle">${subtitle}</h2> `
: ""}
</div>
${currentItem.can_play &&
(!currentItem.thumbnail || !this._narrow)
? html`
<mwc-button
raised
.item=${currentItem}
@click=${this._actionClicked}
>
<ha-svg-icon
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
></ha-svg-icon>
${this.hass.localize(
`ui.components.media-browser.${this.action}`
)}
</mwc-button>
`
: ""}
</div>
</div>
</div>`
: ""
}
<div
class="content"
@scroll=${this._scroll}
@touchmove=${this._scroll}
>
${
this._error
? html`
<div class="container">
${this._renderError(this._error)}
</div>
`
: isTTSMediaSource(currentItem.media_content_id)
? html`
<ha-browse-media-tts
.item=${currentItem}
.hass=${this.hass}
.action=${this.action}
@tts-picked=${this._ttsPicked}
></ha-browse-media-tts>
`
: !children.length && !currentItem.not_shown
? html`
<div class="container no-items">
${currentItem.media_content_id ===
"media-source://media_source/local/."
? html`
<div class="highlight-add-button">
<span>
<ha-svg-icon
.path=${mdiArrowUpRight}
></ha-svg-icon>
</span>
<span>
${this.hass.localize(
"ui.components.media-browser.file_management.highlight_button"
)}
</span>
</div>
`
: this.hass.localize(
"ui.components.media-browser.no_items"
)}
</div>
`
: childrenMediaClass.layout === "grid"
? html`
<div
class="children ${classMap({
portrait:
childrenMediaClass.thumbnail_ratio === "portrait",
})}"
>
${children.map(
(child) => html`
<div
class="child"
.item=${child}
@click=${this._childClicked}
>
<ha-card outlined>
<div class="thumbnail">
${child.thumbnail
? html`
<div
class="${["app", "directory"].includes(
child.media_class
)
? "centered-image"
: ""} image lazythumbnail"
data-src=${child.thumbnail}
></div>
`
: html`
<div class="icon-holder image">
<ha-svg-icon
class="folder"
.path=${MediaClassBrowserSettings[
child.media_class === "directory"
? child.children_media_class ||
child.media_class
: child.media_class
].icon}
></ha-svg-icon>
</div>
`}
${child.can_play
? html`
<ha-icon-button
class="play ${classMap({
can_expand: child.can_expand,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
`
: ""}
</div>
<div class="title">
${child.title}
<paper-tooltip
fitToVisibleBounds
position="top"
offset="4"
>${child.title}</paper-tooltip
>
</div>
</ha-card>
</div>
`
)}
${currentItem.not_shown
? html`
<div class="grid not-shown">
<div class="title">
${this.hass.localize(
"ui.components.media-browser.not_shown",
{ count: currentItem.not_shown }
)}
</div>
</div>
`
: ""}
</div>
`
: html`
<mwc-list>
${children.map(
(child) => html`
<mwc-list-item
@click=${this._childClicked}
.item=${child}
.graphic=${mediaClass.show_list_images
? "medium"
: "avatar"}
dir=${computeRTLDirection(this.hass)}
>
<div
class=${classMap({
graphic: true,
lazythumbnail:
mediaClass.show_list_images === true,
})}
data-src=${ifDefined(
mediaClass.show_list_images && child.thumbnail
? child.thumbnail
: undefined
)}
slot="graphic"
>
<ha-icon-button
class="play ${classMap({
show:
!mediaClass.show_list_images ||
!child.thumbnail,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
</div>
<span class="title">${child.title}</span>
</mwc-list-item>
<li divider role="separator"></li>
`
)}
${currentItem.not_shown
? html`
<mwc-list-item
noninteractive
class="not-shown"
.graphic=${mediaClass.show_list_images
? "medium"
: "avatar"}
dir=${computeRTLDirection(this.hass)}
>
<span class="title">
${this.hass.localize(
"ui.components.media-browser.not_shown",
{ count: currentItem.not_shown }
)}
</span>
</mwc-list-item>
`
: ""}
</mwc-list>
`
}
</div>
</div>
</div>
`;
}
protected firstUpdated(): void {
this._measureCard();
this._attachResizeObserver();
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.size > 1 || !changedProps.has("hass")) {
return true;
}
const oldHass = changedProps.get("hass") as this["hass"];
return oldHass === undefined || oldHass.localize !== this.hass.localize;
}
public willUpdate(changedProps: PropertyValues<this>): void {
super.willUpdate(changedProps);
@@ -260,19 +583,6 @@ export class HaMediaPlayerBrowse extends LitElement {
}
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.size > 1 || !changedProps.has("hass")) {
return true;
}
const oldHass = changedProps.get("hass") as this["hass"];
return oldHass === undefined || oldHass.localize !== this.hass.localize;
}
protected firstUpdated(): void {
this._measureCard();
this._attachResizeObserver();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
@@ -280,368 +590,16 @@ export class HaMediaPlayerBrowse extends LitElement {
this._animateHeaderHeight();
} else if (changedProps.has("_currentItem")) {
this._setHeaderHeight();
this._attachIntersectionObserver();
}
}
protected render(): TemplateResult {
if (this._error) {
return html`
<div class="container">${this._renderError(this._error)}</div>
`;
}
if (!this._currentItem) {
return html`<ha-circular-progress active></ha-circular-progress>`;
}
const currentItem = this._currentItem;
const subtitle = this.hass.localize(
`ui.components.media-browser.class.${currentItem.media_class}`
);
const children = currentItem.children || [];
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
const childrenMediaClass = currentItem.children_media_class
? MediaClassBrowserSettings[currentItem.children_media_class]
: MediaClassBrowserSettings.directory;
const backgroundImage = currentItem.thumbnail
? this._getSignedThumbnail(currentItem.thumbnail).then(
(value) => `url(${value})`
)
: "none";
return html`
${
currentItem.can_play
? html`
<div
class="header ${classMap({
"no-img": !currentItem.thumbnail,
"no-dialog": !this.dialog,
})}"
@transitionend=${this._setHeaderHeight}
>
<div class="header-content">
${currentItem.thumbnail
? html`
<div
class="img"
style="background-image: ${until(
backgroundImage,
""
)}"
>
${this._narrow && currentItem?.can_play
? html`
<ha-fab
mini
.item=${currentItem}
@click=${this._actionClicked}
>
<ha-svg-icon
slot="icon"
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
></ha-svg-icon>
${this.hass.localize(
`ui.components.media-browser.${this.action}`
)}
</ha-fab>
`
: ""}
</div>
`
: html``}
<div class="header-info">
<div class="breadcrumb">
<h1 class="title">${currentItem.title}</h1>
${subtitle
? html` <h2 class="subtitle">${subtitle}</h2> `
: ""}
</div>
${currentItem.can_play &&
(!currentItem.thumbnail || !this._narrow)
? html`
<mwc-button
raised
.item=${currentItem}
@click=${this._actionClicked}
>
<ha-svg-icon
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
></ha-svg-icon>
${this.hass.localize(
`ui.components.media-browser.${this.action}`
)}
</mwc-button>
`
: ""}
</div>
</div>
</div>
`
: ""
}
<div
class="content"
@scroll=${this._scroll}
@touchmove=${this._scroll}
>
${
this._error
? html`
<div class="container">
${this._renderError(this._error)}
</div>
`
: isTTSMediaSource(currentItem.media_content_id)
? html`
<ha-browse-media-tts
.item=${currentItem}
.hass=${this.hass}
.action=${this.action}
@tts-picked=${this._ttsPicked}
></ha-browse-media-tts>
`
: !children.length && !currentItem.not_shown
? html`
<div class="container no-items">
${currentItem.media_content_id ===
"media-source://media_source/local/."
? html`
<div class="highlight-add-button">
<span>
<ha-svg-icon
.path=${mdiArrowUpRight}
></ha-svg-icon>
</span>
<span>
${this.hass.localize(
"ui.components.media-browser.file_management.highlight_button"
)}
</span>
</div>
`
: this.hass.localize(
"ui.components.media-browser.no_items"
)}
</div>
`
: childrenMediaClass.layout === "grid"
? html`
<lit-virtualizer
scroller
.layout=${grid({
itemSize: {
width: "175px",
height: "225px",
},
gap: "16px",
flex: { preserve: "aspect-ratio" },
justify: "space-evenly",
direction: "vertical",
})}
.items=${children}
.renderItem=${this._renderGridItem}
class="children ${classMap({
portrait:
childrenMediaClass.thumbnail_ratio === "portrait",
not_shown: !!currentItem.not_shown,
})}"
></lit-virtualizer>
${currentItem.not_shown
? html`
<div class="grid not-shown">
<div class="title">
${this.hass.localize(
"ui.components.media-browser.not_shown",
{ count: currentItem.not_shown }
)}
</div>
</div>
`
: ""}
`
: html`
<mwc-list>
<lit-virtualizer
scroller
.items=${children}
.renderItem=${this._renderListItem}
></lit-virtualizer>
${currentItem.not_shown
? html`
<mwc-list-item
noninteractive
class="not-shown"
.graphic=${mediaClass.show_list_images
? "medium"
: "avatar"}
dir=${computeRTLDirection(this.hass)}
>
<span class="title">
${this.hass.localize(
"ui.components.media-browser.not_shown",
{ count: currentItem.not_shown }
)}
</span>
</mwc-list-item>
`
: ""}
</mwc-list>
`
}
</div>
</div>
</div>
`;
}
private _renderGridItem = (child: MediaPlayerItem): TemplateResult => {
const backgroundImage = child.thumbnail
? this._getSignedThumbnail(child.thumbnail).then(
(value) => `url(${value})`
)
: "none";
return html`
<div class="child" .item=${child} @click=${this._childClicked}>
<ha-card outlined>
<div class="thumbnail">
${child.thumbnail
? html`
<div
class="${["app", "directory"].includes(child.media_class)
? "centered-image"
: ""} image"
style="background-image: ${until(backgroundImage, "")}"
></div>
`
: html`
<div class="icon-holder image">
<ha-svg-icon
class="folder"
.path=${MediaClassBrowserSettings[
child.media_class === "directory"
? child.children_media_class || child.media_class
: child.media_class
].icon}
></ha-svg-icon>
</div>
`}
${child.can_play
? html`
<ha-icon-button
class="play ${classMap({
can_expand: child.can_expand,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play" ? mdiPlay : mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
`
: ""}
</div>
<div class="title">
${child.title}
<paper-tooltip fitToVisibleBounds position="top" offset="4"
>${child.title}</paper-tooltip
>
</div>
</ha-card>
</div>
`;
};
private _renderListItem = (child: MediaPlayerItem): TemplateResult => {
const currentItem = this._currentItem;
const mediaClass = MediaClassBrowserSettings[currentItem!.media_class];
const backgroundImage =
mediaClass.show_list_images && child.thumbnail
? this._getSignedThumbnail(child.thumbnail).then(
(value) => `url(${value})`
)
: "none";
return html`
<mwc-list-item
@click=${this._childClicked}
.item=${child}
.graphic=${mediaClass.show_list_images ? "medium" : "avatar"}
dir=${computeRTLDirection(this.hass)}
>
<div
class=${classMap({
graphic: true,
thumbnail: mediaClass.show_list_images === true,
})}
style="background-image: ${until(backgroundImage, "")}"
slot="graphic"
>
<ha-icon-button
class="play ${classMap({
show: !mediaClass.show_list_images || !child.thumbnail,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play" ? mdiPlay : mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
</div>
<span class="title">${child.title}</span>
</mwc-list-item>
<li divider role="separator"></li>
`;
};
private async _getSignedThumbnail(
thumbnailUrl: string | undefined
): Promise<string> {
if (!thumbnailUrl) {
return "";
}
if (thumbnailUrl.startsWith("/")) {
// Thumbnails served by local API require authentication
return (await getSignedPath(this.hass, thumbnailUrl)).path;
}
if (thumbnailUrl.startsWith("https://brands.home-assistant.io")) {
// The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon
thumbnailUrl = brandsUrl({
domain: extractDomainFromBrandUrl(thumbnailUrl),
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
});
}
return thumbnailUrl;
}
private _actionClicked = (ev: MouseEvent): void => {
private _actionClicked(ev: MouseEvent): void {
ev.stopPropagation();
const item = (ev.currentTarget as any).item;
this._runAction(item);
};
}
private _runAction(item: MediaPlayerItem): void {
fireEvent(this, "media-picked", { item, navigateIds: this.navigateIds });
@@ -657,7 +615,7 @@ export class HaMediaPlayerBrowse extends LitElement {
});
}
private _childClicked = async (ev: MouseEvent): Promise<void> => {
private async _childClicked(ev: MouseEvent): Promise<void> {
const target = ev.currentTarget as any;
const item: MediaPlayerItem = target.item;
@@ -673,7 +631,7 @@ export class HaMediaPlayerBrowse extends LitElement {
fireEvent(this, "media-browsed", {
ids: [...this.navigateIds, item],
});
};
}
private async _fetchData(
entityId: string,
@@ -700,6 +658,55 @@ export class HaMediaPlayerBrowse extends LitElement {
this._resizeObserver.observe(this);
}
/**
* Load thumbnails for images on demand as they become visible.
*/
private async _attachIntersectionObserver(): Promise<void> {
if (!("IntersectionObserver" in window) || !this._thumbnails) {
return;
}
if (!this._intersectionObserver) {
this._intersectionObserver = new IntersectionObserver(
async (entries, observer) => {
await Promise.all(
entries.map(async (entry) => {
if (!entry.isIntersecting) {
return;
}
const thumbnailCard = entry.target as HTMLElement;
let thumbnailUrl = thumbnailCard.dataset.src;
if (!thumbnailUrl) {
return;
}
if (thumbnailUrl.startsWith("/")) {
// Thumbnails served by local API require authentication
const signedPath = await getSignedPath(this.hass, thumbnailUrl);
thumbnailUrl = signedPath.path;
} else if (
thumbnailUrl.startsWith("https://brands.home-assistant.io")
) {
// The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon
thumbnailUrl = brandsUrl({
domain: extractDomainFromBrandUrl(thumbnailUrl),
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
});
}
thumbnailCard.style.backgroundImage = `url(${thumbnailUrl})`;
observer.unobserve(thumbnailCard); // loaded, so no need to observe anymore
})
);
}
);
}
const observer = this._intersectionObserver!;
for (const thumbnailCard of this._thumbnails) {
observer.observe(thumbnailCard);
}
}
private _closeDialogAction(): void {
fireEvent(this, "close-dialog");
}
@@ -834,7 +841,6 @@ export class HaMediaPlayerBrowse extends LitElement {
.content {
overflow-y: auto;
box-sizing: border-box;
height: 100%;
}
/* HEADER */
@@ -920,7 +926,6 @@ export class HaMediaPlayerBrowse extends LitElement {
.not-shown {
font-style: italic;
color: var(--secondary-text-color);
padding: 8px 16px 8px;
}
.grid.not-shown {
@@ -946,11 +951,7 @@ export class HaMediaPlayerBrowse extends LitElement {
border-bottom-color: var(--divider-color);
}
mwc-list-item {
width: 100%;
}
div.children {
.children {
display: grid;
grid-template-columns: repeat(
auto-fit,
@@ -987,7 +988,7 @@ export class HaMediaPlayerBrowse extends LitElement {
padding-bottom: 100%;
}
.portrait ha-card .thumbnail {
.portrait.children ha-card .thumbnail {
padding-bottom: 150%;
}
@@ -1061,6 +1062,10 @@ export class HaMediaPlayerBrowse extends LitElement {
color: var(--primary-color);
}
ha-card:hover .lazythumbnail {
opacity: 0.5;
}
.child .title {
font-size: 16px;
padding-top: 16px;
@@ -1122,7 +1127,7 @@ export class HaMediaPlayerBrowse extends LitElement {
padding: 0 24px;
}
:host([narrow]) div.children {
:host([narrow]) .children {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important;
}
@@ -1227,16 +1232,6 @@ export class HaMediaPlayerBrowse extends LitElement {
--mdc-fab-box-shadow: none;
--mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5);
}
lit-virtualizer {
height: 100%;
overflow: overlay !important;
contain: size layout !important;
}
lit-virtualizer.not_shown {
height: calc(100% - 36px);
}
`,
];
}

View File

@@ -152,12 +152,6 @@ export interface EventTrigger extends BaseTrigger {
context?: ContextConstraint;
}
export interface CalendarTrigger extends BaseTrigger {
platform: "calendar";
event: "start" | "end";
entity_id: string;
}
export type Trigger =
| StateTrigger
| MqttTrigger
@@ -172,8 +166,7 @@ export type Trigger =
| TimeTrigger
| TemplateTrigger
| EventTrigger
| DeviceTrigger
| CalendarTrigger;
| DeviceTrigger;
interface BaseCondition {
condition: string;
@@ -233,20 +226,6 @@ export interface TriggerCondition extends BaseCondition {
id: string;
}
type ShorthandBaseCondition = Omit<BaseCondition, "condition">;
export interface ShorthandAndCondition extends ShorthandBaseCondition {
and: Condition[];
}
export interface ShorthandOrCondition extends ShorthandBaseCondition {
or: Condition[];
}
export interface ShorthandNotCondition extends ShorthandBaseCondition {
not: Condition[];
}
export type Condition =
| StateCondition
| NumericStateCondition
@@ -258,12 +237,6 @@ export type Condition =
| LogicalCondition
| TriggerCondition;
export type ConditionWithShorthand =
| Condition
| ShorthandAndCondition
| ShorthandOrCondition
| ShorthandNotCondition;
export const triggerAutomationActions = (
hass: HomeAssistant,
entityId: string

View File

@@ -1,9 +1,4 @@
import { HomeAssistant } from "../types";
export interface LogProvider {
key: string;
name: string;
}
export const fetchErrorLog = (hass: HomeAssistant) =>
hass.callApi<string>("GET", "error_log");

View File

@@ -1,15 +1,10 @@
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;
@@ -36,12 +31,8 @@ export const updateUsesProgress = (entity: UpdateEntity): boolean =>
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
typeof entity.attributes.in_progress === "number";
export const updateCanInstall = (
entity: UpdateEntity,
showSkipped = false
): boolean =>
(entity.state === BINARY_STATE_ON ||
(showSkipped && Boolean(entity.attributes.skipped_version))) &&
export const updateCanInstall = (entity: UpdateEntity): boolean =>
entity.state === BINARY_STATE_ON &&
supportsFeature(entity, UPDATE_SUPPORT_INSTALL);
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
@@ -52,75 +43,3 @@ 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"),
});
}
};

View File

@@ -127,7 +127,7 @@ export interface ZWaveJSClient {
export interface ZWaveJSController {
home_id: number;
sdk_version: string;
library_version: string;
type: number;
own_node_id: number;
is_secondary: boolean;
@@ -136,7 +136,7 @@ export interface ZWaveJSController {
was_real_primary: boolean;
is_static_update_controller: boolean;
is_slave: boolean;
firmware_version: string;
serial_api_version: string;
manufacturer_id: number;
product_id: number;
product_type: number;

View File

@@ -146,14 +146,14 @@ export const showOptionsFlowDialog = (
renderMenuHeader(hass, step) {
return (
hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.title`
) || hass.localize(`component.${configEntry.domain}.title`)
`component.${step.handler}.option.step.${step.step_id}.title`
) || hass.localize(`component.${step.handler}.title`)
);
},
renderMenuDescription(hass, step) {
const description = hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.description`,
`component.${step.handler}.option.step.${step.step_id}.description`,
step.description_placeholders
);
return description
@@ -169,7 +169,7 @@ export const showOptionsFlowDialog = (
renderMenuOption(hass, step, option) {
return hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.menu_options.${option}`,
`component.${step.handler}.options.step.${step.step_id}.menu_options.${option}`,
step.description_placeholders
);
},

View File

@@ -77,7 +77,7 @@ class DialogBox extends LitElement {
dialogInitialFocus
.value=${this._value || ""}
@keyup=${this._handleKeyUp}
@input=${this._valueChanged}
@change=${this._valueChanged}
.label=${this._params.inputLabel
? this._params.inputLabel
: ""}

View File

@@ -119,15 +119,7 @@ class MoreInfoVacuum extends LitElement {
"ui.dialogs.more_info_control.vacuum.status"
)}:
</span>
<span>
<strong>
${stateObj.attributes.status ||
this.hass.localize(
`component.vacuum.state._.${stateObj.state}`
) ||
stateObj.state}
</strong>
</span>
<span><strong>${stateObj.attributes.status}</strong></span>
</div>
`
: ""}

View File

@@ -24,7 +24,7 @@ import { domainIcon } from "../../common/entity/domain_icon";
import { navigate } from "../../common/navigate";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import {
defaultFuzzyFilterSort,
fuzzyFilterSort,
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[] =>
defaultFuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
);
static get styles() {

View File

@@ -130,7 +130,7 @@ export class HaTabsSubpageDataTable extends LitElement {
* Array of tabs to show on the page.
* @type {Array}
*/
@property() public tabs: PageNavigation[] = [];
@property() public tabs!: PageNavigation[];
/**
* Force hides the filter menu.
@@ -283,9 +283,6 @@ 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;

View File

@@ -82,16 +82,6 @@ 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`
@@ -144,7 +134,7 @@ class HassTabsSubpage extends LitElement {
this.narrow,
this.localizeFunc || this.hass.localize
);
const showTabs = tabs.length > 1;
const showTabs = tabs.length > 1 || !this.narrow;
return html`
<div class="toolbar">
${this.mainPage || (!this.backPath && history.state?.root)
@@ -169,10 +159,8 @@ class HassTabsSubpage extends LitElement {
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`}
${this.narrow || !showTabs
? html`<div class="main-title">
<slot name="header">${!showTabs ? tabs[0] : ""}</slot>
</div>`
${this.narrow
? html`<div class="main-title"><slot name="header"></slot></div>`
: ""}
${showTabs
? html`
@@ -295,7 +283,6 @@ 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 {

View File

@@ -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;
}
ha-ansi-to-html {
hassio-ansi-to-html {
display: block;
line-height: 22px;
padding: 0 8px;

View File

@@ -332,7 +332,6 @@ 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);
}
`,

View File

@@ -28,7 +28,6 @@ import {
} from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "./types/ha-automation-trigger-calendar";
import "./types/ha-automation-trigger-device";
import "./types/ha-automation-trigger-event";
import "./types/ha-automation-trigger-geo_location";
@@ -45,7 +44,6 @@ import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone";
const OPTIONS = [
"calendar",
"device",
"event",
"state",

View File

@@ -1,79 +0,0 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { CalendarTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import type { TriggerElement } from "../ha-automation-trigger-row";
import type { HaFormSchema } from "../../../../../components/ha-form/types";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
@customElement("ha-automation-trigger-calendar")
export class HaCalendarTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trigger!: CalendarTrigger;
private _schema = memoizeOne((localize: LocalizeFunc) => [
{
name: "entity_id",
required: true,
selector: { entity: { domain: "calendar" } },
},
{
name: "event",
type: "select",
required: true,
options: [
[
"start",
localize(
"ui.panel.config.automation.editor.triggers.type.calendar.start"
),
],
[
"end",
localize(
"ui.panel.config.automation.editor.triggers.type.calendar.end"
),
],
],
},
]);
public static get defaultConfig() {
return {
event: "start" as CalendarTrigger["event"],
};
}
protected render() {
const schema = this._schema(this.hass.localize);
return html`
<ha-form
.schema=${schema}
.data=${this.trigger}
.hass=${this.hass}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const newTrigger = ev.detail.value;
fireEvent(this, "value-changed", { value: newTrigger });
}
private _computeLabelCallback = (schema: HaFormSchema): string =>
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.calendar.${schema.name}`
);
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-calendar": HaCalendarTrigger;
}
}

View File

@@ -80,7 +80,6 @@ class HaConfigBackup extends LitElement {
actions: {
title: "",
width: "15%",
type: "overflow-menu",
template: (_: string, backup: BackupContent) =>
html`<ha-icon-overflow-menu
.hass=${this.hass}
@@ -127,23 +126,17 @@ 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_backups")}
.searchLabel=${this.hass.localize(
"ui.panel.config.backup.picker.search"
)}
.noDataText=${this.hass.localize("ui.panel.config.backup.no_bakcups")}
>
<span slot="header"
>${this.hass.localize("ui.panel.config.backup.caption")}</span
>
<ha-fab
slot="fab"
?disabled=${this._backupData.backing_up}

View File

@@ -224,7 +224,7 @@ class HaBlueprintOverview extends LitElement {
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.automations}
.tabs=${configSections.blueprints}
.columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._processedBlueprints(this.blueprints)}
id="entity_id"

View File

@@ -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,10 +117,6 @@ 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"
];
}

View File

@@ -0,0 +1,342 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { UNIT_C } from "../../../common/const";
import { createCurrencyListEl } from "../../../components/currency-datalist";
import "../../../components/ha-card";
import "../../../components/map/ha-locations-editor";
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
import { createTimezoneListEl } from "../../../components/timezone-datalist";
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
import { SYMBOL_TO_ISO } from "../../../data/currency";
import type { PolymerChangedEvent } from "../../../polymer-types";
import type { HomeAssistant } from "../../../types";
import "../../../components/ha-formfield";
import "../../../components/ha-radio";
import type { HaRadio } from "../../../components/ha-radio";
@customElement("ha-config-core-form")
class ConfigCoreForm extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _working = false;
@state() private _location?: [number, number];
@state() private _currency?: string;
@state() private _elevation?: string;
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
@state() private _timeZone?: string;
protected render(): TemplateResult {
const canEdit = ["storage", "default"].includes(
this.hass.config.config_source
);
const disabled = this._working || !canEdit;
return html`
<ha-card
.header=${this.hass.localize(
"ui.panel.config.core.section.core.form.heading"
)}
>
<div class="card-content">
${!canEdit
? html`
<p>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
)}
</p>
`
: ""}
<div class="row">
<ha-locations-editor
class="flex"
.hass=${this.hass}
.locations=${this._markerLocation(
this.hass.config.latitude,
this.hass.config.longitude,
this._location
)}
@location-updated=${this._locationChanged}
></ha-locations-editor>
</div>
<div class="row">
<div class="flex">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.time_zone"
)}
</div>
<paper-input
class="flex"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.time_zone"
)}
name="timeZone"
list="timezones"
.disabled=${disabled}
.value=${this._timeZoneValue}
@value-changed=${this._handleChange}
></paper-input>
</div>
<div class="row">
<div class="flex">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation"
)}
</div>
<paper-input
class="flex"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation"
)}
name="elevation"
type="number"
.disabled=${disabled}
.value=${this._elevationValue}
@value-changed=${this._handleChange}
>
<span slot="suffix">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_meters"
)}
</span>
</paper-input>
</div>
<div class="row">
<div class="flex">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system"
)}
</div>
<div class="radio-group">
<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._unitSystemValue === "metric"}
@change=${this._unitSystemChanged}
.disabled=${this._working}
></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._unitSystemValue === "imperial"}
@change=${this._unitSystemChanged}
.disabled=${this._working}
></ha-radio>
</ha-formfield>
</div>
</div>
<div class="row">
<div class="flex">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.currency"
)}<br />
<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>
<paper-input
class="flex"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.currency"
)}
name="currency"
list="currencies"
.disabled=${disabled}
.value=${this._currencyValue}
@value-changed=${this._handleChange}
></paper-input>
</div>
</div>
<div class="card-actions">
<mwc-button @click=${this._save} .disabled=${disabled}>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.save_button"
)}
</mwc-button>
</div>
</ha-card>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const tzInput = this.shadowRoot!.querySelector(
"[name=timeZone]"
) as PaperInputElement;
tzInput.inputElement.appendChild(createTimezoneListEl());
const cInput = this.shadowRoot!.querySelector(
"[name=currency]"
) as PaperInputElement;
cInput.inputElement.appendChild(createCurrencyListEl());
}
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 get _currencyValue() {
return this._currency !== undefined
? this._currency
: this.hass.config.currency;
}
private get _elevationValue() {
return this._elevation !== undefined
? this._elevation
: this.hass.config.elevation;
}
private get _timeZoneValue() {
return this._timeZone !== undefined
? this._timeZone
: this.hass.config.time_zone;
}
private get _unitSystemValue() {
return this._unitSystem !== undefined
? this._unitSystem
: this.hass.config.unit_system.temperature === UNIT_C
? "metric"
: "imperial";
}
private _handleChange(ev: PolymerChangedEvent<string>) {
const target = ev.currentTarget as PaperInputElement;
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 _locationChanged(ev) {
this._location = ev.detail.location;
}
private _unitSystemChanged(ev: CustomEvent) {
this._unitSystem = (ev.target as HaRadio).value as "metric" | "imperial";
}
private async _save() {
this._working = true;
try {
const location = this._location || [
this.hass.config.latitude,
this.hass.config.longitude,
];
await saveCoreConfig(this.hass, {
latitude: location[0],
longitude: location[1],
currency: this._currencyValue,
elevation: Number(this._elevationValue),
unit_system: this._unitSystemValue,
time_zone: this._timeZoneValue,
});
} catch (err: any) {
alert(`Error saving config: ${err.message}`);
} finally {
this._working = false;
}
}
static get styles(): CSSResultGroup {
return css`
.row {
display: flex;
flex-direction: row;
margin: 0 -8px;
align-items: center;
}
.secondary {
color: var(--secondary-text-color);
}
.flex {
flex: 1;
}
.row > * {
margin: 0 8px;
}
.radio-group {
display: flex;
flex-direction: column;
flex: 1;
}
.card-actions {
text-align: right;
}
a {
color: var(--primary-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-core-form": ConfigCoreForm;
}
}

View File

@@ -0,0 +1,57 @@
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../layouts/hass-subpage";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../styles/polymer-ha-style";
import "./ha-config-core-form";
import "./ha-config-name-form";
/*
* @appliesMixin LocalizeMixin
*/
class HaConfigCore extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex ha-style">
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
ha-config-name-form,
ha-config-core-form {
display: block;
margin-top: 24px;
}
</style>
<hass-subpage
hass="[[hass]]"
narrow="[[narrow]]"
header="[[localize('ui.panel.config.core.caption')]]"
back-path="/config/system"
>
<div class="content">
<ha-config-name-form hass="[[hass]]"></ha-config-name-form>
<ha-config-core-form hass="[[hass]]"></ha-config-core-form>
</div>
</hass-subpage>
`;
}
static get properties() {
return {
hass: Object,
isWide: Boolean,
narrow: Boolean,
showAdvanced: Boolean,
route: Object,
};
}
}
customElements.define("ha-config-core", HaConfigCore);

View File

@@ -0,0 +1,97 @@
import "@material/mwc-button/mwc-button";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-card";
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
import type { HomeAssistant } from "../../../types";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
@customElement("ha-config-name-form")
class ConfigNameForm extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _working = false;
@state() private _name!: ConfigUpdateValues["location_name"];
protected render(): TemplateResult {
const canEdit = ["storage", "default"].includes(
this.hass.config.config_source
);
const disabled = this._working || !canEdit;
return html`
<ha-card>
<div class="card-content">
${!canEdit
? html`
<p>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
)}
</p>
`
: ""}
<ha-textfield
class="flex"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.location_name"
)}
.disabled=${disabled}
.value=${this._nameValue}
@change=${this._handleChange}
></ha-textfield>
</div>
<div class="card-actions">
<mwc-button @click=${this._save} .disabled=${disabled}>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.save_button"
)}
</mwc-button>
</div>
</ha-card>
`;
}
private get _nameValue() {
return this._name !== undefined
? this._name
: this.hass.config.location_name;
}
private _handleChange(ev) {
const target = ev.currentTarget as HaTextField;
this._name = target.value;
}
private async _save() {
this._working = true;
try {
await saveCoreConfig(this.hass, {
location_name: this._nameValue,
});
} catch (err: any) {
alert("FAIL");
} finally {
this._working = false;
}
}
static get styles() {
return css`
.card-actions {
text-align: right;
}
ha-textfield {
display: block;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-name-form": ConfigNameForm;
}
}

View File

@@ -40,7 +40,7 @@ class ConfigNetwork extends LitElement {
}
return html`
<ha-card outlined header="Network Adapter">
<ha-card outlined header="Network">
<div class="card-content">
${this._error
? html`

View File

@@ -33,11 +33,6 @@ class HaConfigSectionAnalytics extends LitElement {
max-width: 1040px;
margin: 0 auto;
}
ha-config-analytics {
display: block;
max-width: 600px;
margin: 0 auto;
}
`;
}

View File

@@ -1,313 +0,0 @@
import timezones from "google-timezones-json";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { UNIT_C } from "../../../common/const";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { navigate } from "../../../common/navigate";
import { HaProgressButton } from "../../../components/buttons/ha-progress-button";
import { currencies } from "../../../components/currency-datalist";
import "../../../components/ha-formfield";
import "../../../components/ha-radio";
import type { HaRadio } from "../../../components/ha-radio";
import "../../../components/ha-settings-row";
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;
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>
<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>
<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 async _updateEntry(ev) {
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 _editLocation() {
navigate("/config/zone");
}
static styles = [
haStyle,
css`
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
ha-card {
max-width: 500px;
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;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-section-general": HaConfigSectionGeneral;
}
}

View File

@@ -1,12 +1,9 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../types";
import "./ha-config-network";
import "./ha-config-url-form";
import "./supervisor-hostname";
import "./supervisor-network";
@customElement("ha-config-section-network")
class HaConfigSectionNetwork extends LitElement {
@@ -25,13 +22,6 @@ class HaConfigSectionNetwork extends LitElement {
.header=${this.hass.localize("ui.panel.config.network.caption")}
>
<div class="content">
${isComponentLoaded(this.hass, "hassio")
? html`<supervisor-hostname
.hass=${this.hass}
.narrow=${this.narrow}
></supervisor-hostname>
<supervisor-network .hass=${this.hass}></supervisor-network>`
: ""}
<ha-config-url-form .hass=${this.hass}></ha-config-url-form>
<ha-config-network .hass=${this.hass}></ha-config-network>
</div>
@@ -45,14 +35,9 @@ class HaConfigSectionNetwork extends LitElement {
max-width: 1040px;
margin: 0 auto;
}
supervisor-hostname,
supervisor-network,
ha-config-url-form,
ha-config-network {
display: block;
margin: 0 auto;
margin-bottom: 24px;
max-width: 600px;
margin-top: 24px;
}
`;
}

View File

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

View File

@@ -1,208 +0,0 @@
import type { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
import { HassEntities } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
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 { 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 "../dashboard/ha-config-updates";
@customElement("ha-config-section-updates")
class HaConfigSectionUpdates extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@state() private _showSkipped = 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(
this.hass.states,
this._showSkipped
);
return html`
<hass-subpage
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.updates.caption")}
>
<div slot="toolbar-icon">
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.updates.check_updates"
)}
.path=${mdiRefresh}
@click=${this._checkUpdates}
></ha-icon-button>
<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">
${canInstallUpdates.length
? html`
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.updateEntities=${canInstallUpdates}
showAll
></ha-config-updates>
`
: html`
${this.hass.localize("ui.panel.config.updates.no_updates")}
`}
</div>
</ha-card>
</div>
</hass-subpage>
`;
}
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._showSkipped = !this._showSkipped;
break;
case 1:
this._toggleBeta();
break;
}
}
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> {
checkForEntityUpdates(this, this.hass);
}
private _filterUpdateEntitiesWithInstall = memoizeOne(
(entities: HassEntities, showSkipped: boolean) =>
filterUpdateEntitiesWithInstall(entities, showSkipped)
);
static styles = 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;
margin-bottom: max(24px, env(safe-area-inset-bottom));
}
.card-content {
display: flex;
justify-content: space-between;
flex-direction: column;
padding: 16px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-section-updates": HaConfigSectionUpdates;
}
}

View File

@@ -1,12 +1,8 @@
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";
@@ -27,45 +23,29 @@ class HaConfigSystemNavigation extends LitElement {
@property({ type: Boolean }) public showAdvanced!: boolean;
protected render(): TemplateResult {
const pages = configSections.general
.filter((page) => canShowPage(this.hass, page))
.map((page) => ({
...page,
name: page.translationKey
? this.hass.localize(page.translationKey)
: page.name,
}));
const pages = configSections.general.map((page) => ({
...page,
name: page.translationKey
? this.hass.localize(page.translationKey)
: page.name,
}));
return html`
<hass-subpage
back-path="/config"
.header=${this.hass.localize("ui.panel.config.dashboard.system.main")}
.header=${this.hass.localize("ui.panel.config.dashboard.system.title")}
>
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._handleAction}
>
<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>
<ha-card>
${this.narrow
? html`<div class="title">
${this.hass.localize("ui.panel.config.dashboard.system.main")}
${this.hass.localize(
"ui.panel.config.dashboard.system.title"
)}
</div>`
: ""}
<ha-navigation-list
@@ -79,25 +59,13 @@ 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));
}
@@ -110,8 +78,6 @@ class HaConfigSystemNavigation extends LitElement {
ha-card {
overflow: hidden;
margin-bottom: 24px;
margin-bottom: max(24px, env(safe-area-inset-bottom));
}
ha-card a {
@@ -136,7 +102,6 @@ class HaConfigSystemNavigation extends LitElement {
ha-navigation-list {
--navigation-list-item-title-font-size: 16px;
--navigation-list-item-padding: 4px;
}
`,
];

View File

@@ -1,9 +1,9 @@
import type { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiCloudLock, mdiDotsVertical, mdiMagnify } from "@mdi/js";
import { mdiCloudLock, mdiDotsVertical, mdiMagnify, mdiNewBox } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { HassEntities } from "home-assistant-js-websocket";
import type { HassEntities } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
@@ -15,6 +15,8 @@ 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";
@@ -23,17 +25,15 @@ import "../../../components/ha-menu-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tip";
import { CloudStatus } from "../../../data/cloud";
import {
checkForEntityUpdates,
filterUpdateEntitiesWithInstall,
UpdateEntity,
} from "../../../data/update";
import { updateCanInstall, UpdateEntity } from "../../../data/update";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
import "../../../layouts/ha-app-layout";
import { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
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
>
</span>`
<ha-svg-icon class="new" .path=${mdiNewBox}></ha-svg-icon
></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,6 +113,8 @@ class HaConfigDashboard extends LitElement {
@state() private _tip?: string;
private _notifyUpdates = false;
private _pages = memoizeOne((clouStatus, isLoaded) => {
const pages: PageNavigation[] = [];
if (clouStatus && isLoaded) {
@@ -129,8 +131,9 @@ class HaConfigDashboard extends LitElement {
});
protected render(): TemplateResult {
const [canInstallUpdates, totalUpdates] =
this._filterUpdateEntitiesWithInstall(this.hass.states);
const canInstallUpdates = this._filterUpdateEntitiesWithInstall(
this.hass.states
);
return html`
<ha-app-layout>
@@ -170,26 +173,15 @@ class HaConfigDashboard extends LitElement {
full-width
>
${canInstallUpdates.length
? html`<ha-card outlined>
? html`<ha-card>
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.total=${totalUpdates}
.updateEntities=${canInstallUpdates}
></ha-config-updates>
${totalUpdates > canInstallUpdates.length
? html`<a class="button" href="/config/updates">
${this.hass.localize(
"ui.panel.config.updates.more_updates",
{
count: totalUpdates - canInstallUpdates.length,
}
)}
</a>`
: ""}
</ha-card>`
: ""}
<ha-card outlined>
<ha-card>
${this.narrow && canInstallUpdates.length
? html`<div class="title">
${this.hass.localize("panel.config")}
@@ -217,17 +209,60 @@ 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 _filterUpdateEntitiesWithInstall = memoizeOne(
(entities: HassEntities): [UpdateEntity[], number] => {
const updates = filterUpdateEntitiesWithInstall(entities);
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 || ""
);
})
);
return [
updates.slice(0, updates.length === 3 ? updates.length : 2),
updates.length,
];
}
private _filterUpdateEntitiesWithInstall = memoizeOne(
(entities: HassEntities) =>
this._filterUpdateEntities(entities).filter((entity) =>
updateCanInstall(entity)
)
);
private _showQuickBar(): void {
@@ -238,9 +273,27 @@ 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:
checkForEntityUpdates(this, this.hass);
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,
});
break;
}
}
@@ -267,11 +320,6 @@ class HaConfigDashboard extends LitElement {
text-decoration: none;
color: var(--primary-text-color);
}
a.button {
display: block;
color: var(--primary-color);
padding: 16px;
}
.title {
font-size: 16px;
padding: 16px;

View File

@@ -1,6 +1,6 @@
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { canShowPage } from "../../../common/config/can_show_page";
import "../../../components/ha-card";
@@ -30,7 +30,7 @@ class HaConfigNavigation extends LitElement {
name:
page.name ||
this.hass.localize(
`ui.panel.config.dashboard.${page.translationKey}.main`
`ui.panel.config.dashboard.${page.translationKey}.title`
),
description:
page.component === "cloud" && (page.info as CloudStatus)
@@ -51,7 +51,7 @@ class HaConfigNavigation extends LitElement {
${
page.description ||
this.hass.localize(
`ui.panel.config.dashboard.${page.translationKey}.secondary`
`ui.panel.config.dashboard.${page.translationKey}.description`
)
}
`,
@@ -81,12 +81,6 @@ class HaConfigNavigation extends LitElement {
});
}
}
static styles: CSSResultGroup = css`
ha-navigation-list {
--navigation-list-item-title-font-size: 16px;
}
`;
}
declare global {

View File

@@ -1,14 +1,14 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/state-badge";
import "../../../components/ha-alert";
import "../../../components/ha-icon-next";
import type { UpdateEntity } from "../../../data/update";
import type { HomeAssistant } from "../../../types";
import { HomeAssistant } from "../../../types";
@customElement("ha-config-updates")
class HaConfigUpdates extends LitElement {
@@ -19,60 +19,62 @@ class HaConfigUpdates extends LitElement {
@property({ attribute: false })
public updateEntities?: UpdateEntity[];
@property({ type: Number })
public total?: number;
@state() private _showAll = false;
protected render(): TemplateResult {
if (!this.updateEntities?.length) {
return html``;
}
const updates = this.updateEntities;
const updates =
this._showAll || this.updateEntities.length <= 3
? this.updateEntities
: this.updateEntities.slice(0, 2);
return html`
<div class="title">
${this.hass.localize("ui.panel.config.updates.title", {
count: this.total || this.updateEntities.length,
count: this.updateEntities.length,
})}
</div>
<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}
>
${updates.map(
(entity) => html`
<paper-icon-item
@click=${this._openMoreInfo}
.entity_id=${entity.entity_id}
>
<span slot="item-icon" class="icon">
<state-badge
slot="graphic"
.title=${entity.attributes.title ||
entity.attributes.friendly_name}
.stateObj=${entity}
slot="item-icon"
></state-badge>
<span
>${entity.attributes.title ||
entity.attributes.friendly_name}</span
>
<span slot="secondary">
</span>
<paper-item-body two-line>
${entity.attributes.title || entity.attributes.friendly_name}
<div secondary>
${this.hass.localize(
"ui.panel.config.updates.version_available",
{
version_available: entity.attributes.latest_version,
}
)}${entity.attributes.skipped_version
? `(${this.hass.localize("ui.panel.config.updates.skipped")})`
: ""}
</span>
${!this.narrow
? html`<ha-icon-next slot="meta"></ha-icon-next>`
: ""}
</mwc-list-item>
)}
</div>
</paper-item-body>
${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""}
</paper-icon-item>
`
)}
${!this._showAll && this.updateEntities.length >= 4
? html`
<button class="show-more" @click=${this._showAllClicked}>
${this.hass.localize("ui.panel.config.updates.more_updates", {
count: this.updateEntities!.length - updates.length,
})}
</button>
`
)}
</mwc-list>
: ""}
`;
}
@@ -82,19 +84,22 @@ class HaConfigUpdates extends LitElement {
});
}
private _showAllClicked() {
this._showAll = true;
}
static get styles(): CSSResultGroup[] {
return [
css`
:host {
--mdc-list-vertical-padding: 0;
}
.title {
font-size: 16px;
padding: 16px;
padding-bottom: 0;
}
.skipped {
background: var(--secondary-background-color);
.icon {
display: inline-flex;
height: 100%;
align-items: center;
}
ha-icon-next {
color: var(--secondary-text-color);
@@ -117,9 +122,8 @@ class HaConfigUpdates extends LitElement {
outline: none;
text-decoration: underline;
}
mwc-list-item {
paper-icon-item {
cursor: pointer;
font-size: 16px;
}
`,
];

View File

@@ -197,10 +197,7 @@ export class HaConfigDeviceDashboard extends LitElement {
),
model: device.model || "<unknown>",
manufacturer: device.manufacturer || "<unknown>",
area:
device.area_id && areaLookup[device.area_id]
? areaLookup[device.area_id].name
: "—",
area: device.area_id ? areaLookup[device.area_id].name : "—",
integration: device.config_entries.length
? device.config_entries
.filter((entId) => entId in entryLookup)

View File

@@ -276,7 +276,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@selected=${this._deviceClassChanged}
@closed=${stopPropagation}
>
<mwc-list-item></mwc-list-item>
${this._deviceClassesSorted(
domain,
this._deviceClassOptions[0],
@@ -356,25 +355,6 @@ 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}
@@ -392,6 +372,20 @@ 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(

View File

@@ -4,15 +4,14 @@ import {
mdiBadgeAccountHorizontal,
mdiCellphoneCog,
mdiCog,
mdiDatabase,
mdiCpu32Bit,
mdiDevices,
mdiHeart,
mdiHomeAssistant,
mdiInformation,
mdiInformationOutline,
mdiLightningBolt,
mdiMapMarkerRadius,
mdiMathLog,
mdiMemory,
mdiNetwork,
mdiNfcVariant,
mdiPalette,
@@ -20,6 +19,7 @@ import {
mdiPuzzle,
mdiRobot,
mdiScriptText,
mdiServer,
mdiShape,
mdiSofa,
mdiTools,
@@ -68,6 +68,13 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconColor: "#E48629",
components: ["zone"],
},
{
path: "/config/backup",
translationKey: "backup",
iconPath: mdiBackupRestore,
iconColor: "#4084CD",
component: "backup",
},
{
path: "/hassio",
translationKey: "supervisor",
@@ -93,7 +100,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
path: "/config/person",
translationKey: "people",
iconPath: mdiAccount,
iconColor: "#832EA6",
iconColor: "#E48629",
components: ["person", "users"],
},
{
@@ -255,39 +262,48 @@ export const configSections: { [name: string]: PageNavigation[] } = {
],
general: [
{
path: "/config/updates",
translationKey: "ui.panel.config.updates.caption",
iconPath: mdiUpdate,
iconColor: "#3B808E",
component: "core",
path: "/config/core",
translationKey: "ui.panel.config.core.caption",
iconPath: mdiHomeAssistant,
iconColor: "#4A5963",
core: true,
},
{
component: "server_control",
path: "/config/server_control",
translationKey: "ui.panel.config.server_control.caption",
iconPath: mdiServer,
iconColor: "#4A5963",
core: true,
},
{
component: "logs",
path: "/config/logs",
translationKey: "ui.panel.config.logs.caption",
iconPath: mdiMathLog,
iconColor: "#C65326",
iconColor: "#4A5963",
core: true,
},
{
path: "/config/backup",
translationKey: "ui.panel.config.backup.caption",
iconPath: mdiBackupRestore,
iconColor: "#0D47A1",
iconColor: "#4084CD",
component: "backup",
},
{
path: "/hassio/backups",
translationKey: "ui.panel.config.backup.caption",
iconPath: mdiBackupRestore,
iconColor: "#0D47A1",
component: "hassio",
},
{
path: "/config/analytics",
translationKey: "ui.panel.config.analytics.caption",
iconPath: mdiShape,
iconColor: "#f1c447",
},
{
path: "/config/hardware",
translationKey: "ui.panel.config.hardware.caption",
iconPath: mdiCpu32Bit,
iconColor: "#4A5963",
},
{
path: "/config/network",
translationKey: "ui.panel.config.network.caption",
@@ -297,30 +313,14 @@ export const configSections: { [name: string]: PageNavigation[] } = {
{
path: "/config/storage",
translationKey: "ui.panel.config.storage.caption",
iconPath: mdiDatabase,
iconPath: mdiServer,
iconColor: "#518C43",
component: "hassio",
},
{
path: "/config/hardware",
translationKey: "ui.panel.config.hardware.caption",
iconPath: mdiMemory,
iconColor: "#301A8E",
component: "hassio",
},
{
path: "/config/system_health",
translationKey: "ui.panel.config.system_health.caption",
iconPath: mdiHeart,
iconColor: "#507FfE",
components: ["system_health", "hassio"],
},
{
path: "/config/general",
translationKey: "ui.panel.config.core.caption",
iconPath: mdiCog,
iconColor: "#653249",
core: true,
path: "/config/update",
translationKey: "ui.panel.config.updates.caption",
iconPath: mdiUpdate,
iconColor: "#4A5963",
},
],
about: [
@@ -374,6 +374,10 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-cloud",
load: () => import("./cloud/ha-config-cloud"),
},
core: {
tag: "ha-config-core",
load: () => import("./core/ha-config-core"),
},
devices: {
tag: "ha-config-devices",
load: () => import("./devices/ha-config-devices"),
@@ -404,10 +408,6 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-energy",
load: () => import("./energy/ha-config-energy"),
},
hardware: {
tag: "ha-config-hardware",
load: () => import("./hardware/ha-config-hardware"),
},
integrations: {
tag: "ha-config-integrations",
load: () => import("./integrations/ha-config-integrations"),
@@ -418,7 +418,7 @@ class HaPanelConfig extends HassRouterPage {
},
network: {
tag: "ha-config-section-network",
load: () => import("./network/ha-config-section-network"),
load: () => import("./core/ha-config-section-network"),
},
person: {
tag: "ha-config-person",
@@ -436,17 +436,13 @@ 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"),
},
system_health: {
tag: "ha-config-system-health",
load: () => import("./system-health/ha-config-system-health"),
},
updates: {
tag: "ha-config-section-updates",
load: () => import("./core/ha-config-section-updates"),
load: () => import("./core/ha-config-section-storage"),
},
users: {
tag: "ha-config-users",
@@ -456,10 +452,6 @@ 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: () =>
@@ -538,10 +530,6 @@ 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) {

View File

@@ -1,213 +0,0 @@
import { mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { stringCompare } from "../../../common/string/compare";
import "../../../components/ha-dialog";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-icon-next";
import "../../../components/search-input";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import {
fetchHassioHardwareInfo,
HassioHardwareInfo,
} from "../../../data/hassio/hardware";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { dump } from "../../../resources/js-yaml-dump";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
const _filterDevices = memoizeOne(
(showAdvanced: boolean, hardware: HassioHardwareInfo, filter: string) =>
hardware.devices
.filter(
(device) =>
(showAdvanced ||
["tty", "gpio", "input"].includes(device.subsystem)) &&
(device.by_id?.toLowerCase().includes(filter) ||
device.name.toLowerCase().includes(filter) ||
device.dev_path.toLocaleLowerCase().includes(filter) ||
JSON.stringify(device.attributes)
.toLocaleLowerCase()
.includes(filter))
)
.sort((a, b) => stringCompare(a.name, b.name))
);
@customElement("ha-dialog-hardware-available")
class DialogHardwareAvailable extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _hardware?: HassioHardwareInfo;
@state() private _filter?: string;
public async showDialog(): Promise<Promise<void>> {
try {
this._hardware = await fetchHassioHardwareInfo(this.hass);
} catch (err: any) {
await showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.hardware.available_hardware.failed_to_get"
),
text: extractApiErrorMessage(err),
});
}
}
public closeDialog(): void {
this._hardware = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._hardware) {
return html``;
}
const devices = _filterDevices(
this.hass.userData?.showAdvanced || false,
this._hardware,
(this._filter || "").toLowerCase()
);
return html`
<ha-dialog
open
hideActions
@closed=${this.closeDialog}
.heading=${this.hass.localize(
"ui.panel.config.hardware.available_hardware.title"
)}
>
<div class="header" slot="heading">
<h2>
${this.hass.localize(
"ui.panel.config.hardware.available_hardware.title"
)}
</h2>
<ha-icon-button
.label=${this.hass.localize("common.close")}
.path=${mdiClose}
dialogAction="close"
></ha-icon-button>
<search-input
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
.label=${this.hass.localize("common.search")}
>
</search-input>
</div>
${devices.map(
(device) =>
html`
<ha-expansion-panel
.header=${device.name}
.secondary=${device.by_id || undefined}
outlined
>
<div class="device-property">
<span>
${this.hass.localize(
"ui.panel.config.hardware.available_hardware.subsystem"
)}:
</span>
<span>${device.subsystem}</span>
</div>
<div class="device-property">
<span>
${this.hass.localize(
"ui.panel.config.hardware.available_hardware.device_path"
)}:
</span>
<code>${device.dev_path}</code>
</div>
${device.by_id
? html`
<div class="device-property">
<span>
${this.hass.localize(
"ui.panel.config.hardware.available_hardware.id"
)}:
</span>
<code>${device.by_id}</code>
</div>
`
: ""}
<div class="attributes">
<span>
${this.hass.localize(
"ui.panel.config.hardware.available_hardware.attributes"
)}:
</span>
<pre>${dump(device.attributes, { indent: 2 })}</pre>
</div>
</ha-expansion-panel>
`
)}
</ha-dialog>
`;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-icon-button {
position: absolute;
right: 16px;
top: 10px;
text-decoration: none;
color: var(--primary-text-color);
}
h2 {
margin: 18px 42px 0 18px;
color: var(--primary-text-color);
}
ha-expansion-panel {
margin: 4px 0;
}
pre,
code {
background-color: var(--markdown-code-background-color, none);
border-radius: 3px;
}
pre {
padding: 16px;
overflow: auto;
line-height: 1.45;
font-family: var(--code-font-family, monospace);
}
code {
font-size: 85%;
padding: 0.2em 0.4em;
}
search-input {
margin: 8px 16px 0;
display: block;
}
.device-property {
display: flex;
justify-content: space-between;
}
.attributes {
margin-top: 12px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-dialog-hardware-available": DialogHardwareAvailable;
}
}

View File

@@ -1,256 +0,0 @@
import "@material/mwc-list/mwc-list-item";
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";
import "../../../components/buttons/ha-progress-button";
import "../../../components/ha-alert";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-settings-row";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../data/hassio/common";
import {
fetchHassioHassOsInfo,
fetchHassioHostInfo,
HassioHassOSInfo,
HassioHostInfo,
rebootHost,
shutdownHost,
} from "../../../data/hassio/host";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { showhardwareAvailableDialog } from "./show-dialog-hardware-available";
@customElement("ha-config-hardware")
class HaConfigHardware extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@state() private _error?: { code: string; message: string };
@state() private _OSData?: HassioHassOSInfo;
@state() private _hostData?: HassioHostInfo;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
if (isComponentLoaded(this.hass, "hassio")) {
this._load();
}
}
protected render(): TemplateResult {
return html`
<hass-subpage
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.hardware.caption")}
>
${this._error
? html`
<ha-alert alert-type="error"
>${this._error.message || this._error.code}</ha-alert
>
`
: ""}
${this._OSData && this._hostData
? html`
<div class="content">
<ha-card outlined>
<div class="card-content">
<ha-settings-row>
<span slot="heading"
>${this.hass.localize(
"ui.panel.config.hardware.board"
)}</span
>
<div slot="description">
<span class="value">${this._OSData.board}</span>
</div>
</ha-settings-row>
</div>
<div class="card-actions">
<div class="buttons">
${this._hostData.features.includes("reboot")
? html`
<ha-progress-button
class="warning"
@click=${this._hostReboot}
>
${this.hass.localize(
"ui.panel.config.hardware.reboot_host"
)}
</ha-progress-button>
`
: ""}
${this._hostData.features.includes("shutdown")
? html`
<ha-progress-button
class="warning"
@click=${this._hostShutdown}
>
${this.hass.localize(
"ui.panel.config.hardware.shutdown_host"
)}
</ha-progress-button>
`
: ""}
</div>
<ha-button-menu corner="BOTTOM_START">
<ha-icon-button
.label=${this.hass.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<mwc-list-item
.action=${"hardware"}
@click=${this._openHardware}
>
${this.hass.localize(
"ui.panel.config.hardware.available_hardware.title"
)}
</mwc-list-item>
</ha-button-menu>
</div>
</ha-card>
</div>
`
: ""}
</hass-subpage>
`;
}
private async _load() {
try {
this._OSData = await fetchHassioHassOsInfo(this.hass);
this._hostData = await fetchHassioHostInfo(this.hass);
} catch (err: any) {
this._error = err.message || err;
}
}
private async _openHardware() {
showhardwareAvailableDialog(this);
}
private async _hostReboot(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.config.hardware.reboot_host"),
text: this.hass.localize("ui.panel.config.hardware.reboot_host_confirm"),
confirmText: this.hass.localize("ui.panel.config.hardware.reboot_host"),
dismissText: this.hass.localize("common.cancel"),
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await rebootHost(this.hass);
} catch (err: any) {
// Ignore connection errors, these are all expected
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.hardware.failed_to_reboot_host"
),
text: extractApiErrorMessage(err),
});
}
}
button.progress = false;
}
private async _hostShutdown(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.config.hardware.shutdown_host"),
text: this.hass.localize(
"ui.panel.config.hardware.shutdown_host_confirm"
),
confirmText: this.hass.localize("ui.panel.config.hardware.shutdown_host"),
dismissText: this.hass.localize("common.cancel"),
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await shutdownHost(this.hass);
} catch (err: any) {
// Ignore connection errors, these are all expected
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.hardware.failed_to_shutdown_host"
),
text: extractApiErrorMessage(err),
});
}
}
button.progress = false;
}
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;
}
ha-button-menu {
color: var(--secondary-text-color);
--mdc-menu-min-width: 200px;
}
.card-actions {
height: 48px;
display: flex;
justify-content: space-between;
align-items: center;
}
.buttons {
display: flex;
align-items: center;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-hardware": HaConfigHardware;
}
}

View File

@@ -1,12 +0,0 @@
import { fireEvent } from "../../../common/dom/fire_event";
export const loadHardwareAvailableDialog = () =>
import("./dialog-hardware-available");
export const showhardwareAvailableDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", {
dialogTag: "ha-dialog-hardware-available",
dialogImport: loadHardwareAvailableDialog,
dialogParams: {},
});
};

View File

@@ -79,11 +79,9 @@ class HaInputSelectForm extends LitElement {
"ui.dialogs.helper_settings.generic.icon"
)}
></ha-icon-picker>
<div class="header">
${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.options"
)}:
</div>
${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.options"
)}:
${this._options.length
? this._options.map(
(option, index) => html`
@@ -208,10 +206,6 @@ class HaInputSelectForm extends LitElement {
#option_input {
margin-top: 8px;
}
.header {
margin-top: 8px;
margin-bottom: 8px;
}
`,
];
}

View File

@@ -1,19 +1,12 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { property } from "lit/decorators";
import "../../../components/ha-logo-svg";
import {
fetchHassioHostInfo,
fetchHassioHassOsInfo,
HassioHassOSInfo,
HassioHostInfo,
} from "../../../data/hassio/host";
import { HassioInfo, fetchHassioInfo } from "../../../data/hassio/supervisor";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import "./integrations-card";
import "./system-health-card";
const JS_TYPE = __BUILD__;
const JS_VERSION = __VERSION__;
@@ -29,12 +22,6 @@ class HaConfigInfo extends LitElement {
@property() public route!: Route;
@state() private _hostInfo?: HassioHostInfo;
@state() private _osInfo?: HassioHassOSInfo;
@state() private _hassioInfo?: HassioInfo;
protected render(): TemplateResult {
const hass = this.hass;
const customUiList: Array<{ name: string; url: string; version: string }> =
@@ -61,19 +48,7 @@ class HaConfigInfo extends LitElement {
</ha-logo-svg>
</a>
<br />
<h2>Home Assistant Core ${hass.connection.haVersion}</h2>
${this._hassioInfo
? html`<h2>
Home Assistant Supervisor ${this._hassioInfo.supervisor}
</h2>`
: ""}
${this._osInfo?.version
? html`<h2>Home Assistant OS ${this._osInfo.version}</h2>`
: ""}
${this._hostInfo
? html`<h4>Kernel version ${this._hostInfo.kernel}</h4>
<h4>Agent version ${this._hostInfo.agent_version}</h4>`
: ""}
<h2>Home Assistant ${hass.connection.haVersion}</h2>
<p>
${this.hass.localize(
"ui.panel.config.info.path_configuration",
@@ -136,30 +111,33 @@ class HaConfigInfo extends LitElement {
"type",
JS_TYPE
)}
${customUiList.length > 0
? html`
<div>
${this.hass.localize("ui.panel.config.info.custom_uis")}
${customUiList.map(
(item) => html`
<div>
<a href=${item.url} target="_blank"> ${item.name}</a>:
${item.version}
</div>
`
)}
</div>
`
: ""}
${
customUiList.length > 0
? html`
<div>
${this.hass.localize("ui.panel.config.info.custom_uis")}
${customUiList.map(
(item) => html`
<div>
<a href=${item.url} target="_blank"> ${item.name}</a
>: ${item.version}
</div>
`
)}
</div>
`
: ""
}
</p>
</div>
<div>
<system-health-card .hass=${this.hass}></system-health-card>
<integrations-card
.hass=${this.hass}
.narrow=${this.narrow}
></integrations-card>
</div>
</hass-subpage>
</hass-tabs-subpage>
`;
}
@@ -173,22 +151,6 @@ class HaConfigInfo extends LitElement {
this.requestUpdate();
}
}, 1000);
if (isComponentLoaded(this.hass, "hassio")) {
this._loadSupervisorInfo();
}
}
private async _loadSupervisorInfo(): Promise<void> {
const [hostInfo, osInfo, hassioInfo] = await Promise.all([
fetchHassioHostInfo(this.hass),
fetchHassioHassOsInfo(this.hass),
fetchHassioInfo(this.hass),
]);
this._hassioInfo = hassioInfo;
this._osInfo = osInfo;
this._hostInfo = hostInfo;
}
static get styles(): CSSResultGroup {
@@ -218,6 +180,7 @@ class HaConfigInfo extends LitElement {
color: var(--primary-color);
}
system-health-card,
integrations-card {
display: block;
max-width: 600px;

View File

@@ -0,0 +1,298 @@
import "@material/mwc-button/mwc-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiContentCopy } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatDateTime } from "../../../common/datetime/format_date_time";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress";
import "../../../components/ha-icon-button";
import { domainToName } from "../../../data/integration";
import {
subscribeSystemHealthInfo,
SystemCheckValueObject,
SystemHealthInfo,
} from "../../../data/system_health";
import { HomeAssistant } from "../../../types";
import { showToast } from "../../../util/toast";
const sortKeys = (a: string, b: string) => {
if (a === "homeassistant") {
return -1;
}
if (b === "homeassistant") {
return 1;
}
if (a < b) {
return -1;
}
if (b < a) {
return 1;
}
return 0;
};
class SystemHealthCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _info?: SystemHealthInfo;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
const sections: TemplateResult[] = [];
if (!this._info) {
sections.push(
html`
<div class="loading-container">
<ha-circular-progress active></ha-circular-progress>
</div>
`
);
} else {
const domains = Object.keys(this._info).sort(sortKeys);
for (const domain of domains) {
const domainInfo = this._info[domain];
const keys: TemplateResult[] = [];
for (const key of Object.keys(domainInfo.info)) {
let value: unknown;
if (
domainInfo.info[key] &&
typeof domainInfo.info[key] === "object"
) {
const info = domainInfo.info[key] as SystemCheckValueObject;
if (info.type === "pending") {
value = html`
<ha-circular-progress active size="tiny"></ha-circular-progress>
`;
} else if (info.type === "failed") {
value = html`
<span class="error">${info.error}</span>${!info.more_info
? ""
: html`
<a
href=${info.more_info}
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.config.info.system_health.more_info"
)}
</a>
`}
`;
} else if (info.type === "date") {
value = formatDateTime(new Date(info.value), this.hass.locale);
}
} else {
value = domainInfo.info[key];
}
keys.push(html`
<tr>
<td>
${this.hass.localize(
`component.${domain}.system_health.info.${key}`
) || key}
</td>
<td>${value}</td>
</tr>
`);
}
if (domain !== "homeassistant") {
sections.push(
html`
<div class="card-header">
<h3>${domainToName(this.hass.localize, domain)}</h3>
${!domainInfo.manage_url
? ""
: html`
<a class="manage" href=${domainInfo.manage_url}>
<mwc-button>
${this.hass.localize(
"ui.panel.config.info.system_health.manage"
)}
</mwc-button>
</a>
`}
</div>
`
);
}
sections.push(html`
<table>
${keys}
</table>
`);
}
}
return html`
<ha-card>
<h1 class="card-header">
<div class="card-header-text">
${domainToName(this.hass.localize, "system_health")}
</div>
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._copyInfo}
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.panel.config.info.copy_menu")}
.path=${mdiContentCopy}
></ha-icon-button>
<mwc-list-item>
${this.hass.localize("ui.panel.config.info.copy_raw")}
</mwc-list-item>
<mwc-list-item>
${this.hass.localize("ui.panel.config.info.copy_github")}
</mwc-list-item>
</ha-button-menu>
</h1>
<div class="card-content">${sections}</div>
</ha-card>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.hass!.loadBackendTranslation("system_health");
if (!isComponentLoaded(this.hass!, "system_health")) {
this._info = {
system_health: {
info: {
error: this.hass.localize(
"ui.panel.config.info.system_health_error"
),
},
},
};
return;
}
subscribeSystemHealthInfo(this.hass!, (info) => {
this._info = info;
});
}
private async _copyInfo(ev: CustomEvent<ActionDetail>): Promise<void> {
const github = ev.detail.index === 1;
let haContent: string | undefined;
const domainParts: string[] = [];
for (const domain of Object.keys(this._info!).sort(sortKeys)) {
const domainInfo = this._info![domain];
let first = true;
const parts = [
`${
github && domain !== "homeassistant"
? `<details><summary>${domainToName(
this.hass.localize,
domain
)}</summary>\n`
: ""
}`,
];
for (const key of Object.keys(domainInfo.info)) {
let value: unknown;
if (typeof domainInfo.info[key] === "object") {
const info = domainInfo.info[key] as SystemCheckValueObject;
if (info.type === "pending") {
value = "pending";
} else if (info.type === "failed") {
value = `failed to load: ${info.error}`;
} else if (info.type === "date") {
value = formatDateTime(new Date(info.value), this.hass.locale);
}
} else {
value = domainInfo.info[key];
}
if (github && first) {
parts.push(`${key} | ${value}\n-- | --`);
first = false;
} else {
parts.push(`${key}${github ? " | " : ": "}${value}`);
}
}
if (domain === "homeassistant") {
haContent = parts.join("\n");
} else {
domainParts.push(parts.join("\n"));
if (github && domain !== "homeassistant") {
domainParts.push("</details>");
}
}
}
await copyToClipboard(
`${github ? "## " : ""}System Health\n${haContent}\n\n${domainParts.join(
"\n\n"
)}`
);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
static get styles(): CSSResultGroup {
return css`
table {
width: 100%;
}
td:first-child {
width: 45%;
}
td:last-child {
direction: ltr;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
}
.card-header {
justify-content: space-between;
display: flex;
align-items: center;
}
.error {
color: var(--error-color);
}
a {
color: var(--primary-color);
}
a.manage {
text-decoration: none;
}
`;
}
}
customElements.define("system-health-card", SystemHealthCard);

View File

@@ -111,7 +111,7 @@ const groupByIntegration = (
class HaConfigIntegrations extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
@property() public narrow!: boolean;
@property() public isWide!: boolean;
@@ -709,9 +709,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
return [
haStyle,
css`
:host([narrow]) hass-tabs-subpage {
--main-title-margin: 0;
}
ha-button-menu {
margin-left: 8px;
}

View File

@@ -214,9 +214,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
<br />
<span>${item.property}</span>
</span>
<span slot="heading" .title=${item.metadata.label}>
${item.metadata.label}
</span>
<span slot="heading">${item.metadata.label}</span>
<span slot="description">
${item.metadata.description}
${item.metadata.description !== null && !item.metadata.writeable

View File

@@ -1,142 +1,98 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { mdiRefresh } from "@mdi/js";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
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 { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators";
import "../../../components/ha-icon-button";
import "../../../components/ha-select";
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";
@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[] | TemplateResult | string;
@state() private _error?: string;
@state() private _errorHTML!: TemplateResult[] | string;
protected render(): TemplateResult {
return html`
<div class="error-log-intro">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${this._logHTML
${this._errorHTML
? html`
<ha-card outlined>
<div class="header">
<h2>
${this.hass.localize("ui.panel.config.logs.full_logs")}
</h2>
<ha-icon-button
.path=${mdiRefresh}
@click=${this._refresh}
.label=${this.hass.localize("ui.common.refresh")}
></ha-icon-button>
</div>
<div class="card-content error-log">${this._logHTML}</div>
<ha-card>
<ha-icon-button
.path=${mdiRefresh}
@click=${this._refreshErrorLog}
.label=${this.hass.localize("ui.common.refresh")}
></ha-icon-button>
<div class="card-content error-log">${this._errorHTML}</div>
</ha-card>
`
: ""}
${!this._logHTML
? html`
<mwc-button
raised
@click=${this._refreshLogs}
dir=${computeRTLDirection(this.hass)}
>
${this.hass.localize("ui.panel.config.logs.load_logs")}
: html`
<mwc-button raised @click=${this._refreshErrorLog}>
${this.hass.localize("ui.panel.config.logs.load_full_log")}
</mwc-button>
`
: ""}
`}
</div>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (this.hass?.config.safe_mode || this.show) {
if (this.hass?.config.safe_mode) {
this.hass.loadFragmentTranslation("config");
this._refreshLogs();
this._refreshErrorLog();
}
}
protected updated(changedProps) {
super.updated(changedProps);
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();
if (changedProps.has("filter") && this._isLogLoaded) {
this._refreshErrorLog();
}
}
private async _refresh(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
await this._refreshLogs();
button.progress = false;
}
private async _refreshLogs(): Promise<void> {
this._logHTML = this.hass.localize("ui.panel.config.logs.loading_log");
let log: string;
if (isComponentLoaded(this.hass, "hassio")) {
try {
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.provider,
"error",
extractApiErrorMessage(err)
);
return;
static get styles(): CSSResultGroup {
return css`
.error-log-intro {
text-align: center;
margin: 16px;
}
} else {
log = await fetchErrorLog(this.hass!);
}
ha-icon-button {
float: right;
}
.error-log {
font-family: var(--code-font-family, monospace);
clear: both;
text-align: left;
padding-top: 12px;
}
.error-log > div:hover {
background-color: var(--secondary-background-color);
}
.error {
color: var(--error-color);
}
.warning {
color: var(--warning-color);
}
`;
}
private async _refreshErrorLog(): Promise<void> {
this._errorHTML = this.hass.localize("ui.panel.config.logs.loading_log");
const log = await fetchErrorLog(this.hass!);
this._isLogLoaded = true;
this._logHTML = log
this._errorHTML = log
? log
.split("\n")
.filter((entry) => {
@@ -163,61 +119,6 @@ class ErrorLogCard extends LitElement {
})
: this.hass.localize("ui.panel.config.logs.no_errors");
}
static styles: CSSResultGroup = css`
.error-log-intro {
text-align: center;
margin: 16px;
}
.header {
display: flex;
justify-content: space-between;
padding: 16px;
}
ha-select {
display: block;
max-width: 500px;
width: 100%;
}
ha-icon-button {
float: right;
}
.error-log {
font-family: var(--code-font-family, monospace);
clear: both;
text-align: left;
padding-top: 12px;
}
.error-log > div {
overflow: auto;
overflow-wrap: break-word;
}
.error-log > div:hover {
background-color: var(--secondary-background-color);
}
.error {
color: var(--error-color);
}
.warning {
color: var(--warning-color);
}
:host-context([style*="direction: rtl;"]) mwc-button {
direction: rtl;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"error-log-card": ErrorLogCard;
}
}
customElements.define("error-log-card", ErrorLogCard);

View File

@@ -1,11 +1,7 @@
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";
@@ -14,51 +10,22 @@ 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({ type: Boolean }) public narrow!: boolean;
@property() public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public isWide!: boolean;
@property({ type: Boolean }) public showAdvanced!: boolean;
@property() public showAdvanced!: boolean;
@property({ attribute: false }) public route!: Route;
@property() 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) {
@@ -101,60 +68,21 @@ 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">
${this._selectedLogProvider === "core"
? html`
<system-log-card
.hass=${this.hass}
.filter=${this._filter}
></system-log-card>
`
: ""}
<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,
@@ -180,11 +108,6 @@ export class HaConfigLogs extends LitElement {
.content {
direction: ltr;
}
mwc-button[slot="trigger"] {
--mdc-theme-primary: var(--primary-text-color);
--mdc-icon-size: 36px;
}
`,
];
}

View File

@@ -18,7 +18,6 @@ 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 {
@@ -76,7 +75,7 @@ export class SystemLogCard extends LitElement {
: [];
return html`
<div class="system-log-intro">
<ha-card outlined>
<ha-card>
${this._items === undefined
? html`
<div class="loading-container">
@@ -132,7 +131,7 @@ export class SystemLogCard extends LitElement {
`
)}
<div class="card-actions" dir=${computeRTLDirection(this.hass)}>
<div class="card-actions">
<ha-call-service-button
.hass=${this.hass}
domain="system_log"
@@ -205,10 +204,6 @@ export class SystemLogCard extends LitElement {
.warning {
color: var(--warning-color);
}
:host-context([style*="direction: rtl;"]) .card-actions {
direction: rtl;
}
`;
}
}

View File

@@ -1,7 +1,6 @@
import {
mdiCheck,
mdiCheckCircleOutline,
mdiDotsVertical,
mdiOpenInNew,
mdiPlus,
} from "@mdi/js";
@@ -17,7 +16,6 @@ import {
DataTableColumnContainer,
RowClickedEvent,
} from "../../../../components/data-table/ha-data-table";
import "../../../../components/ha-clickable-list-item";
import "../../../../components/ha-fab";
import "../../../../components/ha-icon";
import "../../../../components/ha-icon-button";
@@ -218,7 +216,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
if (isComponentLoaded(this.hass, "energy")) {
result.push({
icon: "hass:lightning-bolt",
title: this.hass.localize(`ui.panel.config.dashboard.energy.main`),
title: this.hass.localize(`ui.panel.config.dashboard.energy.title`),
show_in_sidebar: true,
mode: "storage",
url_path: "energy",
@@ -262,32 +260,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
hasFab
clickable
>
${this.hass.userData?.showAdvanced
? html`
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
activatable
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-clickable-list-item
@click=${this._entryClicked}
href="/config/lovelace/resources"
aria-label=${this.hass.localize(
"ui.panel.config.lovelace.resources.caption"
)}
>
${this.hass.localize(
"ui.panel.config.lovelace.resources.caption"
)}
</ha-clickable-list-item>
</ha-button-menu>
`
: ""}
<ha-fab
slot="fab"
.label=${this.hass.localize(
@@ -382,8 +354,4 @@ export class HaConfigLovelaceDashboards extends LitElement {
},
});
}
private _entryClicked(ev) {
ev.currentTarget.blur();
}
}

View File

@@ -12,6 +12,13 @@ export const lovelaceTabs = [
translationKey: "ui.panel.config.lovelace.dashboards.caption",
icon: "hass:view-dashboard",
},
{
component: "lovelace",
path: "/config/lovelace/resources",
translationKey: "ui.panel.config.lovelace.resources.caption",
icon: "hass:file-multiple",
advancedOnly: true,
},
];
@customElement("ha-config-lovelace")

View File

@@ -77,6 +77,7 @@ 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)}

View File

@@ -1,121 +0,0 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-alert";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-formfield";
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 {
changeHostOptions,
fetchHassioHostInfo,
} from "../../../data/hassio/host";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types";
@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;
protected firstUpdated() {
this._fetchHostInfo();
}
private async _fetchHostInfo() {
const hostInfo = await fetchHassioHostInfo(this.hass);
this._hostname = hostInfo.hostname;
}
protected render(): TemplateResult {
if (!this._hostname) {
return html``;
}
return html`
<ha-card
class="no-padding"
outlined
.header=${this.hass.localize("ui.panel.config.network.hostname.title")}
>
<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
>
<ha-textfield
.disabled=${this._processing}
.value=${this._hostname}
@change=${this._handleChange}
placeholder="homeassistant"
>
</ha-textfield>
</ha-settings-row>
</div>
<div class="card-actions">
<mwc-button @click=${this._save} .disabled=${this._processing}>
${this._processing
? html`<ha-circular-progress active size="small">
</ha-circular-progress>`
: this.hass.localize("ui.common.save")}
</mwc-button>
</div>
</ha-card>
`;
}
private _handleChange(ev) {
this._hostname = ev.target.value;
}
private async _save() {
this._processing = true;
try {
await changeHostOptions(this.hass, { hostname: this._hostname });
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.network.hostname.failed_to_set_hostname"
),
text: extractApiErrorMessage(err),
});
} finally {
this._processing = false;
}
}
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 {
interface HTMLElementTagNameMap {
"supervisor-hostname": HassioHostname;
}
}

View File

@@ -1,577 +0,0 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import "../../../components/ha-alert";
import "../../../components/ha-circular-progress";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-formfield";
import "../../../components/ha-header-bar";
import "../../../components/ha-icon-button";
import "../../../components/ha-radio";
import "../../../components/ha-related-items";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import {
AccessPoints,
accesspointScan,
fetchNetworkInfo,
NetworkInterface,
updateNetworkInterface,
WifiConfiguration,
} from "../../../data/hassio/network";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types";
import "../../../components/ha-card";
const IP_VERSIONS = ["ipv4", "ipv6"];
@customElement("supervisor-network")
export class HassioNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _accessPoints?: AccessPoints;
@state() private _curTabIndex = 0;
@state() private _dirty = false;
@state() private _interface?: NetworkInterface;
@state() private _interfaces!: NetworkInterface[];
@state() private _processing = false;
@state() private _scanning = false;
@state() private _wifiConfiguration?: WifiConfiguration;
protected firstUpdated() {
this._fetchNetworkInfo();
}
private async _fetchNetworkInfo() {
const network = await fetchNetworkInfo(this.hass);
this._interfaces = network.interfaces.sort((a, b) =>
a.primary > b.primary ? -1 : 1
);
this._interface = { ...this._interfaces[this._curTabIndex] };
}
protected render(): TemplateResult {
if (!this._interface) {
return html``;
}
return html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.network.supervisor.title"
)}
>
${this._interfaces.length > 1
? html`<mwc-tab-bar
.activeIndex=${this._curTabIndex}
@MDCTabBar:activated=${this._handleTabActivated}
>${this._interfaces.map(
(device) =>
html`<mwc-tab
.id=${device.interface}
.label=${device.interface}
>
</mwc-tab>`
)}
</mwc-tab-bar>`
: ""}
${cache(this._renderTab())}
</ha-card>
`;
}
private _renderTab() {
return html`<div class="card-content">
${IP_VERSIONS.map((version) =>
this._interface![version] ? this._renderIPConfiguration(version) : ""
)}
${this._interface?.type === "wireless"
? html`
<ha-expansion-panel header="Wi-Fi" outlined>
${this._interface?.wifi?.ssid
? html`<p>
${this.hass.localize(
"ui.panel.config.network.supervisor.connected_to",
"ssid",
this._interface?.wifi?.ssid
)}
</p>`
: ""}
<mwc-button
class="scan"
@click=${this._scanForAP}
.disabled=${this._scanning}
>
${this._scanning
? html`<ha-circular-progress active size="small">
</ha-circular-progress>`
: this.hass.localize(
"ui.panel.config.network.supervisor.scan_ap"
)}
</mwc-button>
${this._accessPoints &&
this._accessPoints.accesspoints &&
this._accessPoints.accesspoints.length !== 0
? html`
<mwc-list>
${this._accessPoints.accesspoints
.filter((ap) => ap.ssid)
.map(
(ap) =>
html`
<mwc-list-item
twoline
@click=${this._selectAP}
.activated=${ap.ssid ===
this._wifiConfiguration?.ssid}
.ap=${ap}
>
<span>${ap.ssid}</span>
<span slot="secondary">
${ap.mac} - Strength: ${ap.signal}
</span>
</mwc-list-item>
`
)}
</mwc-list>
`
: ""}
${this._wifiConfiguration
? html`
<div class="radio-row">
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.open"
)}
>
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="open"
name="auth"
.checked=${this._wifiConfiguration.auth ===
undefined ||
this._wifiConfiguration.auth === "open"}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.wep"
)}
>
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="wep"
name="auth"
.checked=${this._wifiConfiguration.auth === "wep"}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.wpa"
)}
>
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="wpa-psk"
name="auth"
.checked=${this._wifiConfiguration.auth ===
"wpa-psk"}
>
</ha-radio>
</ha-formfield>
</div>
${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep"
? html`
<paper-input
class="flex-auto"
type="password"
id="psk"
label="Password"
version="wifi"
@value-changed=${this
._handleInputValueChangedWifi}
>
</paper-input>
`
: ""}
`
: ""}
</ha-expansion-panel>
`
: ""}
${this._dirty
? html`<ha-alert alert-type="warning">
${this.hass.localize(
"ui.panel.config.network.supervisor.warning"
)}
</ha-alert>`
: ""}
</div>
<div class="card-actions">
<mwc-button @click=${this._updateNetwork} .disabled=${!this._dirty}>
${this._processing
? html`<ha-circular-progress active size="small">
</ha-circular-progress>`
: this.hass.localize("ui.common.save")}
</mwc-button>
</div>`;
}
private _selectAP(event) {
this._wifiConfiguration = event.currentTarget.ap;
this._dirty = true;
}
private async _scanForAP() {
if (!this._interface) {
return;
}
this._scanning = true;
try {
this._accessPoints = await accesspointScan(
this.hass,
this._interface.interface
);
} catch (err: any) {
showAlertDialog(this, {
title: "Failed to scan for accesspoints",
text: extractApiErrorMessage(err),
});
} finally {
this._scanning = false;
}
}
private _renderIPConfiguration(version: string) {
return html`
<ha-expansion-panel
.header=${`IPv${version.charAt(version.length - 1)}`}
outlined
>
<div class="radio-row">
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.dhcp"
)}
>
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="auto"
name="${version}method"
.checked=${this._interface![version]?.method === "auto"}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.static"
)}
>
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="static"
name="${version}method"
.checked=${this._interface![version]?.method === "static"}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.disabled"
)}
class="warning"
>
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="disabled"
name="${version}method"
.checked=${this._interface![version]?.method === "disabled"}
>
</ha-radio>
</ha-formfield>
</div>
${this._interface![version].method === "static"
? html`
<paper-input
class="flex-auto"
id="address"
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.ip_netmask"
)}
.version=${version}
.value=${this._toString(this._interface![version].address)}
@value-changed=${this._handleInputValueChanged}
>
</paper-input>
<paper-input
class="flex-auto"
id="gateway"
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.gateway"
)}
.version=${version}
.value=${this._interface![version].gateway}
@value-changed=${this._handleInputValueChanged}
>
</paper-input>
<paper-input
class="flex-auto"
id="nameservers"
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.dns_servers"
)}
.version=${version}
.value=${this._toString(this._interface![version].nameservers)}
@value-changed=${this._handleInputValueChanged}
>
</paper-input>
`
: ""}
</ha-expansion-panel>
`;
}
_toArray(data: string | string[]): string[] {
if (Array.isArray(data)) {
if (data && typeof data[0] === "string") {
data = data[0];
}
}
if (!data) {
return [];
}
if (typeof data === "string") {
return data.replace(/ /g, "").split(",");
}
return data;
}
_toString(data: string | string[]): string {
if (!data) {
return "";
}
if (Array.isArray(data)) {
return data.join(", ");
}
return data;
}
private async _updateNetwork() {
this._processing = true;
let interfaceOptions: Partial<NetworkInterface> = {};
IP_VERSIONS.forEach((version) => {
interfaceOptions[version] = {
method: this._interface![version]?.method || "auto",
};
if (this._interface![version]?.method === "static") {
interfaceOptions[version] = {
...interfaceOptions[version],
address: this._toArray(this._interface![version]?.address),
gateway: this._interface![version]?.gateway,
nameservers: this._toArray(this._interface![version]?.nameservers),
};
}
});
if (this._wifiConfiguration) {
interfaceOptions = {
...interfaceOptions,
wifi: {
ssid: this._wifiConfiguration.ssid,
mode: this._wifiConfiguration.mode,
auth: this._wifiConfiguration.auth || "open",
},
};
if (interfaceOptions.wifi!.auth !== "open") {
interfaceOptions.wifi = {
...interfaceOptions.wifi,
psk: this._wifiConfiguration.psk,
};
}
}
interfaceOptions.enabled =
this._wifiConfiguration !== undefined ||
interfaceOptions.ipv4?.method !== "disabled" ||
interfaceOptions.ipv6?.method !== "disabled";
try {
await updateNetworkInterface(
this.hass,
this._interface!.interface,
interfaceOptions
);
this._dirty = false;
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.network.supervisor.failed_to_change"
),
text: extractApiErrorMessage(err),
});
} finally {
this._processing = false;
}
}
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
if (this._dirty) {
const confirm = await showConfirmationDialog(this, {
text: this.hass.localize("ui.panel.config.network.supervisor.unsaved"),
confirmText: this.hass.localize("ui.common.yes"),
dismissText: this.hass.localize("ui.common.no"),
});
if (!confirm) {
this.requestUpdate("_interface");
return;
}
}
this._curTabIndex = ev.detail.index;
this._interface = { ...this._interfaces[ev.detail.index] };
}
private _handleRadioValueChanged(ev: CustomEvent): void {
const value = (ev.target as any).value as "disabled" | "auto" | "static";
const version = (ev.target as any).version as "ipv4" | "ipv6";
if (
!value ||
!this._interface ||
this._interface[version]!.method === value
) {
return;
}
this._dirty = true;
this._interface[version]!.method = value;
this.requestUpdate("_interface");
}
private _handleRadioValueChangedAp(ev: CustomEvent): void {
const value = (ev.target as any).value as string as
| "open"
| "wep"
| "wpa-psk";
this._wifiConfiguration!.auth = value;
this._dirty = true;
this.requestUpdate("_wifiConfiguration");
}
private _handleInputValueChanged(ev: CustomEvent): void {
const value: string | null | undefined = (ev.target as PaperInputElement)
.value;
const version = (ev.target as any).version as "ipv4" | "ipv6";
const id = (ev.target as PaperInputElement).id;
if (
!value ||
!this._interface ||
this._toString(this._interface[version]![id]) === this._toString(value)
) {
return;
}
this._dirty = true;
this._interface[version]![id] = value;
}
private _handleInputValueChangedWifi(ev: CustomEvent): void {
const value: string | null | undefined = (ev.target as PaperInputElement)
.value;
const id = (ev.target as PaperInputElement).id;
if (
!value ||
!this._wifiConfiguration ||
this._wifiConfiguration![id] === value
) {
return;
}
this._dirty = true;
this._wifiConfiguration![id] = value;
}
static get styles(): CSSResultGroup {
return [
css`
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
}
mwc-tab-bar {
border-bottom: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
margin-bottom: 24px;
}
.content {
display: block;
padding: 20px 24px;
}
mwc-button.warning {
--mdc-theme-primary: var(--error-color);
}
mwc-button.scan {
margin-left: 8px;
}
:host([rtl]) app-toolbar {
direction: rtl;
text-align: right;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 16px;
margin: 4px 0;
}
paper-input {
padding: 0 14px;
}
mwc-list-item {
--mdc-list-side-padding: 10px;
}
.card-actions {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
align-items: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-network": HassioNetwork;
}
}

View File

@@ -0,0 +1,266 @@
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
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
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
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;
}
}

View File

@@ -1,200 +0,0 @@
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-circular-progress";
import "../../../components/ha-markdown";
import "../../../components/ha-select";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../data/hassio/common";
import {
DatadiskList,
fetchHassioHassOsInfo,
HassioHassOSInfo,
HassioHostInfo,
listDatadisks,
moveDatadisk,
} from "../../../data/hassio/host";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { MoveDatadiskDialogParams } from "./show-dialog-move-datadisk";
const calculateMoveTime = memoizeOne((hostInfo: HassioHostInfo): number => {
const speed = hostInfo.disk_life_time !== "" ? 30 : 10;
const moveTime = (hostInfo.disk_used * 1000) / 60 / speed;
const rebootTime = (hostInfo.startup_time * 4) / 60;
return Math.ceil((moveTime + rebootTime) / 10) * 10;
});
@customElement("dialog-move-datadisk")
class MoveDatadiskDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _hostInfo?: HassioHostInfo;
@state() private _selectedDevice?: string;
@state() private _devices?: DatadiskList["devices"];
@state() private _osInfo?: HassioHassOSInfo;
@state() private _moving = false;
public async showDialog(
dialogParams: MoveDatadiskDialogParams
): Promise<Promise<void>> {
this._hostInfo = dialogParams.hostInfo;
try {
this._osInfo = await fetchHassioHassOsInfo(this.hass);
} catch (err: any) {
await showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.hardware.available_hardware.failed_to_get"
),
text: extractApiErrorMessage(err),
});
}
listDatadisks(this.hass).then((data) => {
this._devices = data.devices;
});
}
public closeDialog(): void {
this._selectedDevice = undefined;
this._devices = undefined;
this._moving = false;
this._hostInfo = undefined;
this._osInfo = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._hostInfo || !this._osInfo) {
return html``;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
.heading=${this._moving
? this.hass.localize("ui.panel.config.storage.datadisk.moving")
: this.hass.localize("ui.panel.config.storage.datadisk.title")}
@closed=${this.closeDialog}
?hideActions=${this._moving}
>
${this._moving
? html` <ha-circular-progress alt="Moving" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this.hass.localize(
"ui.panel.config.storage.datadisk.moving_desc"
)}
</p>`
: html` ${this._devices?.length
? html`
${this.hass.localize(
"ui.panel.config.storage.datadisk.description",
{
current_path: this._osInfo.data_disk,
time: calculateMoveTime(this._hostInfo),
}
)}
<br /><br />
<ha-select
.label=${this.hass.localize(
"ui.panel.config.storage.datadisk.select_device"
)}
@selected=${this._select_device}
dialogInitialFocus
>
${this._devices.map(
(device) =>
html`<mwc-list-item .value=${device}
>${device}</mwc-list-item
>`
)}
</ha-select>
`
: this._devices === undefined
? this.hass.localize(
"ui.panel.config.storage.datadisk.loading_devices"
)
: this.hass.localize(
"ui.panel.config.storage.datadisk.no_devices"
)}
<mwc-button
slot="secondaryAction"
@click=${this.closeDialog}
dialogInitialFocus
>
${this.hass.localize("ui.panel.config.storage.datadisk.cancel")}
</mwc-button>
<mwc-button
.disabled=${!this._selectedDevice}
slot="primaryAction"
@click=${this._moveDatadisk}
>
${this.hass.localize("ui.panel.config.storage.datadisk.move")}
</mwc-button>`}
</ha-dialog>
`;
}
private _select_device(ev) {
this._selectedDevice = ev.target.value;
}
private async _moveDatadisk() {
this._moving = true;
try {
await moveDatadisk(this.hass, this._selectedDevice!);
} catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.storage.datadisk.failed_to_move"
),
text: extractApiErrorMessage(err),
});
this.closeDialog();
}
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-select {
width: 100%;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-move-datadisk": MoveDatadiskDialog;
}
}

View File

@@ -1,149 +0,0 @@
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";
import "../../../components/ha-alert";
import "../../../components/ha-metric";
import { fetchHassioHostInfo, HassioHostInfo } from "../../../data/hassio/host";
import "../../../layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../types";
import {
getValueInPercentage,
roundWithOneDecimal,
} from "../../../util/calculate";
import "../core/ha-config-analytics";
import { showMoveDatadiskDialog } from "./show-dialog-move-datadisk";
@customElement("ha-config-section-storage")
class HaConfigSectionStorage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@state() private _error?: { code: string; message: string };
@state() private _hostInfo?: HassioHostInfo;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
if (isComponentLoaded(this.hass, "hassio")) {
this._load();
}
}
protected render(): TemplateResult {
return html`
<hass-subpage
back-path="/config/system"
.hass=${this.hass}
.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`
<ha-alert alert-type="error"
>${this._error.message || this._error.code}</ha-alert
>
`
: ""}
${this._hostInfo
? html`
<ha-card outlined>
<div class="card-content">
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.storage.used_space"
)}
.value=${this._getUsedSpace(
this._hostInfo?.disk_used,
this._hostInfo?.disk_total
)}
.tooltip=${`${this._hostInfo.disk_used} GB/${this._hostInfo.disk_total} GB`}
></ha-metric>
${this._hostInfo.disk_life_time !== "" &&
this._hostInfo.disk_life_time >= 10
? html`
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.storage.emmc_lifetime_used"
)}
.value=${this._hostInfo.disk_life_time}
.tooltip=${`${
this._hostInfo.disk_life_time - 10
} % -
${this._hostInfo.disk_life_time} %`}
class="emmc"
></ha-metric>
`
: ""}
</div>
</ha-card>
`
: ""}
</div>
</hass-subpage>
`;
}
private async _load() {
try {
this._hostInfo = await fetchHassioHostInfo(this.hass);
} catch (err: any) {
this._error = err.message || err;
}
}
private _moveDatadisk(): void {
showMoveDatadiskDialog(this, {
hostInfo: this._hostInfo!,
});
}
private _getUsedSpace = (used: number, total: number) =>
roundWithOneDecimal(getValueInPercentage(used, 0, total));
static styles = css`
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
ha-card {
max-width: 600px;
margin: 0 auto;
justify-content: space-between;
flex-direction: column;
display: flex;
}
.card-content {
display: flex;
justify-content: space-between;
flex-direction: column;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-section-storage": HaConfigSectionStorage;
}
}

View File

@@ -1,17 +0,0 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { HassioHostInfo } from "../../../data/hassio/host";
export interface MoveDatadiskDialogParams {
hostInfo: HassioHostInfo;
}
export const showMoveDatadiskDialog = (
element: HTMLElement,
dialogParams: MoveDatadiskDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-move-datadisk",
dialogImport: () => import("./dialog-move-datadisk"),
dialogParams,
});
};

View File

@@ -1,508 +0,0 @@
import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiContentCopy } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket/dist/types";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatDateTime } from "../../../common/datetime/format_date_time";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
import { subscribePollingCollection } from "../../../common/util/subscribe-polling";
import "../../../components/ha-alert";
import "../../../components/ha-button-menu";
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) => {
if (a === "homeassistant") {
return -1;
}
if (b === "homeassistant") {
return 1;
}
if (a < b) {
return -1;
}
if (b < a) {
return 1;
}
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;
@property({ type: Boolean }) public narrow!: boolean;
@state() private _info?: SystemHealthInfo;
@state() private _supervisorStats?: HassioStats;
@state() private _resolutionInfo?: HassioResolution;
@state() private _coreStats?: HassioStats;
@state() private _error?: { code: string; message: string };
public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> {
const subs: Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> = [];
if (isComponentLoaded(this.hass, "system_health")) {
subs.push(
subscribeSystemHealthInfo(this.hass!, (info) => {
this._info = info;
})
);
}
if (isComponentLoaded(this.hass, "hassio")) {
subs.push(
subscribePollingCollection(
this.hass,
async () => {
this._supervisorStats = await fetchHassioStats(
this.hass,
"supervisor"
);
this._coreStats = await fetchHassioStats(this.hass, "core");
},
10000
)
);
fetchHassioResolution(this.hass).then((data) => {
this._resolutionInfo = data;
});
}
return subs;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.hass!.loadBackendTranslation("system_health");
}
protected render(): TemplateResult {
const sections: TemplateResult[] = [];
if (!this._info) {
sections.push(
html`
<div class="loading-container">
<ha-circular-progress active></ha-circular-progress>
</div>
`
);
} else {
const domains = Object.keys(this._info).sort(sortKeys);
for (const domain of domains) {
const domainInfo = this._info[domain];
const keys: TemplateResult[] = [];
for (const key of Object.keys(domainInfo.info)) {
let value: unknown;
if (
domainInfo.info[key] &&
typeof domainInfo.info[key] === "object"
) {
const info = domainInfo.info[key] as SystemCheckValueObject;
if (info.type === "pending") {
value = html`
<ha-circular-progress active size="tiny"></ha-circular-progress>
`;
} else if (info.type === "failed") {
value = html`
<span class="error">${info.error}</span>${!info.more_info
? ""
: html`
<a
href=${info.more_info}
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.config.info.system_health.more_info"
)}
</a>
`}
`;
} else if (info.type === "date") {
value = formatDateTime(new Date(info.value), this.hass.locale);
}
} else {
value = domainInfo.info[key];
}
keys.push(html`
<tr>
<td>
${this.hass.localize(
`component.${domain}.system_health.info.${key}`
) || key}
</td>
<td>${value}</td>
</tr>
`);
}
if (domain !== "homeassistant") {
sections.push(
html`
<div class="card-header">
<h3>${domainToName(this.hass.localize, domain)}</h3>
${!domainInfo.manage_url
? ""
: html`
<a class="manage" href=${domainInfo.manage_url}>
<mwc-button>
${this.hass.localize(
"ui.panel.config.info.system_health.manage"
)}
</mwc-button>
</a>
`}
</div>
`
);
}
sections.push(html`
<table>
${keys}
</table>
`);
}
}
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/system"
.header=${this.hass.localize("ui.panel.config.system_health.caption")}
>
${this._error
? html`
<ha-alert alert-type="error"
>${this._error.message || this._error.code}</ha-alert
>
`
: ""}
${this._info
? html`
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._copyInfo}
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.panel.config.info.copy_menu")}
.path=${mdiContentCopy}
></ha-icon-button>
<mwc-list-item>
${this.hass.localize("ui.panel.config.info.copy_raw")}
</mwc-list-item>
<mwc-list-item>
${this.hass.localize("ui.panel.config.info.copy_github")}
</mwc-list-item>
</ha-button-menu>
`
: ""}
<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>
${!this._coreStats && !this._supervisorStats
? ""
: html`
<ha-card outlined>
<div class="card-content">
${this._coreStats
? html`
<h3>
${this.hass.localize(
"ui.panel.config.system_health.core_stats"
)}
</h3>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.cpu_usage"
)}
.value=${this._coreStats.cpu_percent}
></ha-metric>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.ram_usage"
)}
.value=${this._coreStats.memory_percent}
></ha-metric>
`
: ""}
${this._supervisorStats
? html`
<h3>
${this.hass.localize(
"ui.panel.config.system_health.supervisor_stats"
)}
</h3>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.cpu_usage"
)}
.value=${this._supervisorStats.cpu_percent}
></ha-metric>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.ram_usage"
)}
.value=${this._supervisorStats.memory_percent}
></ha-metric>
`
: ""}
</div>
</ha-card>
`}
</div>
</hass-subpage>
`;
}
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;
const domainParts: string[] = [];
for (const domain of Object.keys(this._info!).sort(sortKeys)) {
const domainInfo = this._info![domain];
let first = true;
const parts = [
`${
github && domain !== "homeassistant"
? `<details><summary>${domainToName(
this.hass.localize,
domain
)}</summary>\n`
: ""
}`,
];
for (const key of Object.keys(domainInfo.info)) {
let value: unknown;
if (typeof domainInfo.info[key] === "object") {
const info = domainInfo.info[key] as SystemCheckValueObject;
if (info.type === "pending") {
value = "pending";
} else if (info.type === "failed") {
value = `failed to load: ${info.error}`;
} else if (info.type === "date") {
value = formatDateTime(new Date(info.value), this.hass.locale);
}
} else {
value = domainInfo.info[key];
}
if (github && first) {
parts.push(`${key} | ${value}\n-- | --`);
first = false;
} else {
parts.push(`${key}${github ? " | " : ": "}${value}`);
}
}
if (domain === "homeassistant") {
haContent = parts.join("\n");
} else {
domainParts.push(parts.join("\n"));
if (github && domain !== "homeassistant") {
domainParts.push("</details>");
}
}
}
await copyToClipboard(
`${github ? "## " : ""}System Health\n${haContent}\n\n${domainParts.join(
"\n\n"
)}`
);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
static styles: CSSResultGroup = css`
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
ha-card {
display: block;
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%;
}
td:first-child {
width: 45%;
}
td:last-child {
direction: ltr;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
}
.card-header {
justify-content: space-between;
display: flex;
align-items: center;
}
.error {
color: var(--error-color);
}
a {
color: var(--primary-color);
}
a.manage {
text-decoration: none;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-system-health": HaConfigSystemHealth;
}
}

View File

@@ -157,7 +157,7 @@ export class HaConfigUsers extends LitElement {
.narrow=${this.narrow}
.route=${this.route}
backPath="/config"
.tabs=${configSections.persons}
.tabs=${configSections.areas}
.columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._users}
@row-click=${this._editUser}

View File

@@ -13,6 +13,7 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { navigate } from "../../../common/navigate";
@@ -36,10 +37,7 @@ import {
Zone,
ZoneMutableParams,
} from "../../../data/zone";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
@@ -187,26 +185,36 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
<div style="display:inline-block">
<ha-icon-button
.entityId=${stateObject.entity_id}
.noEdit=${stateObject.entity_id !== "zone.home" ||
!this._canEditCore}
@click=${this._openCoreConfig}
disabled=${ifDefined(
stateObject.entity_id === "zone.home" &&
this.narrow &&
this._canEditCore
? undefined
: true
)}
.path=${stateObject.entity_id === "zone.home" &&
this.narrow &&
this._canEditCore
? mdiPencil
: mdiPencilOff}
.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}
.label=${hass.localize(
"ui.panel.config.zone.edit_zone"
)}
></ha-icon-button>
${stateObject.entity_id !== "zone.home"
? html`
<paper-tooltip animation-delay="0" position="left">
${hass.localize(
"ui.panel.config.zone.configured_in_yaml"
)}
</paper-tooltip>
`
: ""}
<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>
</div>
</paper-icon-item>
`
@@ -383,16 +391,22 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
this._openDialog(entry);
}
private async _openCoreConfig(ev) {
if (ev.currentTarget.noEdit) {
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.zone.can_not_edit"),
text: this.hass.localize("ui.panel.config.zone.configured_in_yaml"),
confirm: () => {},
});
private async _openCoreConfig(ev: Event) {
const entityId: string = (ev.currentTarget! as any).entityId;
if (entityId !== "zone.home" || !this.narrow || !this._canEditCore) {
return;
}
navigate("/config/general");
if (
!(await showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.config.zone.go_to_core_config"),
text: this.hass.localize("ui.panel.config.zone.home_zone_core_config"),
confirmText: this.hass!.localize("ui.common.yes"),
dismissText: this.hass!.localize("ui.common.no"),
}))
) {
return;
}
navigate("/config/core");
}
private async _createEntry(values: ZoneMutableParams) {

View File

@@ -41,10 +41,6 @@ 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"),
},
},
};

View File

@@ -67,9 +67,6 @@ 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

View File

@@ -1,240 +0,0 @@
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>
<mwc-button raised @click=${this._validateConfig}>
${this.hass.localize(
"ui.panel.developer-tools.tabs.yaml.section.validation.check_config"
)}
</mwc-button>
</div>
<div id="configLog" class="validate-log">
${this._validateLog}
</div>
`}
</div>
<div class="card-actions">
<mwc-button
@click=${this._validateConfig}
.disabled=${this._validateLog}
>
${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: 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;
}
.validate-log {
white-space: pre-line;
direction: ltr;
}
.content {
padding: 28px 20px 0;
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;
}
}

View File

@@ -174,26 +174,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
if (this._config!.needle) {
return undefined;
}
// new format
let segments = this._config!.segments;
if (segments) {
segments = [...segments].sort((a, b) => a?.from - b?.from);
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
if (
segment &&
numberValue >= segment.from &&
(i + 1 === segments.length || numberValue < segments[i + 1]?.from)
) {
return segment.color;
}
}
return severityMap.normal;
}
// old format
const sections = this._config!.severity;
if (!sections) {
@@ -226,16 +206,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
}
private _severityLevels() {
// new format
const segments = this._config!.segments;
if (segments) {
return segments.map((segment) => ({
level: segment?.from,
stroke: segment?.color,
}));
}
// old format
const sections = this._config!.severity;
if (!sections) {

View File

@@ -176,11 +176,6 @@ export interface SeverityConfig {
red?: number;
}
export interface GaugeSegment {
from: number;
color: string;
}
export interface GaugeCardConfig extends LovelaceCardConfig {
entity: string;
name?: string;
@@ -190,7 +185,6 @@ export interface GaugeCardConfig extends LovelaceCardConfig {
severity?: SeverityConfig;
theme?: string;
needle?: boolean;
segments?: GaugeSegment[];
}
export interface ConfigEntity extends EntityConfig {

View File

@@ -1,17 +1,10 @@
import { promiseTimeout } from "../../../common/util/promise-timeout";
import { LovelaceCard, LovelaceHeaderFooter } from "../types";
export const computeCardSize = (
card: LovelaceCard | LovelaceHeaderFooter
): number | Promise<number> => {
if (typeof card.getCardSize === "function") {
try {
return promiseTimeout(500, card.getCardSize()).catch(
() => 1
) as Promise<number>;
} catch (_e: any) {
return 1;
}
return card.getCardSize();
}
if (customElements.get(card.localName)) {
return 1;

View File

@@ -2,7 +2,6 @@ import "../../../../components/ha-form/ha-form";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
array,
assert,
assign,
boolean,
@@ -19,11 +18,6 @@ import type { GaugeCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
const gaugeSegmentStruct = object({
from: number(),
color: string(),
});
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
@@ -35,7 +29,6 @@ const cardConfigStruct = assign(
severity: optional(object()),
theme: optional(string()),
needle: optional(boolean()),
segments: optional(array(gaugeSegmentStruct)),
})
);

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