Compare commits

..

31 Commits

Author SHA1 Message Date
Ludeeus 294967014d Add pointer cursor to ha-formfield in update card 2021-11-29 06:01:17 +00:00
Philip Allgaier 366aa8aed1 Fix typo on config page + adjust icon color (#10713) 2021-11-28 17:52:39 +01:00
Joakim Sørensen 43011179eb Finish up config changes (#10710)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-11-26 17:24:30 +01:00
Joakim Sørensen 6177d2b416 Use app-header-text-color (#10711) 2021-11-26 17:11:06 +01:00
Joakim Sørensen f70485bc49 Don't make button disabled on error (#10699) 2021-11-25 16:56:57 +01:00
Erik Montnemery 921763b5f1 Improve device information when via device is unknown (#10685) 2021-11-24 09:09:21 +01:00
Joakim Sørensen 5fd4315789 Fix addon slug (#10693) 2021-11-23 08:53:17 -08:00
Joakim Sørensen ed291b57d0 Render update card on add-on page (#10681) 2021-11-23 08:18:40 -08:00
Joakim Sørensen f833701e7c Update background colors of navigation icons (#10691) 2021-11-23 14:36:11 +01:00
Paulus Schoutsen 8533b90957 Bumped version to 20211123.0 2021-11-22 17:28:13 -08:00
Laszlo Magyar c95a54c6f3 Fixing typo in #10626 (#10686) 2021-11-22 18:59:35 +01:00
Joakim Sørensen a991640f52 Remove first part of the update description (#10669) 2021-11-22 09:09:23 -08:00
Joakim Sørensen 3d99b92c07 Limit setting up supervisor subscriptions to the supervisor panel (#10680) 2021-11-22 08:59:28 -08:00
Philip Allgaier d28ad17135 Use component to ensure relative-time in Glance card gets updated (#10666) 2021-11-22 11:12:04 +01:00
Philip Allgaier 3c67fc96b1 Make "Show more" show everything starting from yesterday (#10533) 2021-11-22 10:56:40 +01:00
Joakim Sørensen 4719636176 Fix dark main-content and split gallery demo (#10675) 2021-11-21 21:01:51 -08:00
Paulus Schoutsen 45efee28b8 Add scenes and scripts as buttons in footer of area cards (#10673)
* Add scenes and scripts as chips in footer of area cards

* Remove unused chips config type

* Update src/panels/lovelace/common/generate-lovelace-config.ts

Co-authored-by: Zack Barett <arnett.zackary@gmail.com>

* Fix typing

Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
2021-11-21 20:59:56 -08:00
Joakim Sørensen 3bcf225380 Fix color overlay in ha-alert content (#10674) 2021-11-21 20:16:19 +01:00
Joakim Sørensen 2e81f843ce Use white for icons with backgound (#10672) 2021-11-21 18:07:55 +00:00
Joakim Sørensen a430142296 Add iconColor to ha-config-navigation entries (#10658) 2021-11-21 09:52:58 -08:00
Joakim Sørensen 6335b13c5e Remove core note on update page (#10661) 2021-11-21 09:16:06 -08:00
Joakim Sørensen 6c4e987a24 Make ha-chip-set slot-able (#10647) 2021-11-21 09:15:38 -08:00
Joakim Sørensen 1a5c43d72a Fix color over slotted image in ha-alert (#10652) 2021-11-21 09:13:48 -08:00
epenet 91dbfca899 Add frequency device class for sensor (#10621) 2021-11-21 05:05:32 +01:00
Bram Kragten 96f103644a Send error message to sender (#10660) 2021-11-19 13:22:49 -08:00
Paulus Schoutsen 5304e5a670 Always render groups/areas in a single column (#10655) 2021-11-19 13:16:43 -08:00
Lasse Rosenow 390e5b3881 Simplify launch screen svg (#10643) 2021-11-18 16:20:45 -08:00
Joakim Sørensen 9f5756c9fa Use ha-formfield around backup checkbox (#10653) 2021-11-18 16:09:39 -08:00
Joakim Sørensen 0ca35d7012 Remove ha-alert actionText (#10646) 2021-11-18 16:09:13 -08:00
Joakim Sørensen 0d19f4792f Fix active tab (#10654) 2021-11-18 19:21:19 +00:00
Joakim Sørensen 91b009af79 Fix back button color (#10650) 2021-11-18 18:57:15 +01:00
61 changed files with 1188 additions and 804 deletions
+38 -4
View File
@@ -13,7 +13,11 @@ import {
ShowDemoMessage,
ShowLovelaceViewMessage,
} from "../../../../src/cast/receiver_messages";
import { ReceiverStatusMessage } from "../../../../src/cast/sender_messages";
import {
ReceiverErrorCode,
ReceiverErrorMessage,
ReceiverStatusMessage,
} from "../../../../src/cast/sender_messages";
import { atLeastVersion } from "../../../../src/common/config/version";
import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click";
import {
@@ -134,6 +138,26 @@ export class HcMain extends HassElement {
}
}
private _sendError(
error_code: number,
error_message: string,
senderId?: string
) {
const error: ReceiverErrorMessage = {
type: "receiver_error",
error_code,
error_message,
};
if (senderId) {
this.sendMessage(senderId, error);
} else {
for (const sender of castContext.getSenders()) {
this.sendMessage(sender.id, error);
}
}
}
private _dialogClosed = () => {
document.body.setAttribute("style", "overflow-y: auto !important");
};
@@ -156,14 +180,18 @@ export class HcMain extends HassElement {
}),
});
} catch (err: any) {
this._error = this._getErrorMessage(err);
const errorMessage = this._getErrorMessage(err);
this._error = errorMessage;
this._sendError(err, errorMessage);
return;
}
let connection;
try {
connection = await createConnection({ auth });
} catch (err: any) {
this._error = this._getErrorMessage(err);
const errorMessage = this._getErrorMessage(err);
this._error = errorMessage;
this._sendError(err, errorMessage);
return;
}
if (this.hass) {
@@ -181,8 +209,10 @@ export class HcMain extends HassElement {
if (!this.hass) {
this._sendStatus(msg.senderId!);
this._error = "Cannot show Lovelace because we're not connected.";
this._sendError(ReceiverErrorCode.NOT_CONNECTED, this._error);
return;
}
this._error = undefined;
if (msg.urlPath === "lovelace") {
msg.urlPath = null;
}
@@ -204,10 +234,14 @@ export class HcMain extends HassElement {
this._handleNewLovelaceConfig(lovelaceConfig)
);
} catch (err: any) {
if (err.code !== "config_not_found") {
if (
atLeastVersion(this.hass.connection.haVersion, 0, 107) &&
err.code !== "config_not_found"
) {
// eslint-disable-next-line
console.log("Error fetching Lovelace configuration", err, msg);
this._error = `Error fetching Lovelace configuration: ${err.message}`;
this._sendError(ReceiverErrorCode.FETCH_CONFIG_FAILED, this._error);
return;
}
// Generate a Lovelace config.
File diff suppressed because one or more lines are too long
+73 -32
View File
@@ -1,15 +1,16 @@
import "../../../src/components/ha-logo-svg";
import { html, css, LitElement, TemplateResult } from "lit";
import "@material/mwc-button/mwc-button";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-card";
import "../../../src/components/ha-logo-svg";
const alerts: {
title?: string;
description: string | TemplateResult;
type: "info" | "warning" | "error" | "success";
dismissable?: boolean;
action?: string;
rtl?: boolean;
iconSlot?: TemplateResult;
actionSlot?: TemplateResult;
@@ -76,19 +77,29 @@ const alerts: {
title: "Error with action",
description: "This is a test error alert with action",
type: "error",
action: "restart",
actionSlot: html`<mwc-button slot="action" label="restart"></mwc-button>`,
},
{
title: "Unsaved data",
description: "You have unsaved data",
type: "warning",
action: "save",
actionSlot: html`<mwc-button slot="action" label="save"></mwc-button>`,
},
{
title: "Slotted icon",
description: "Alert with slotted icon",
type: "warning",
iconSlot: html`<ha-logo-svg slot="icon"></ha-logo-svg>`,
iconSlot: html`<span slot="icon" class="image">
<ha-logo-svg></ha-logo-svg>
</span>`,
},
{
title: "Slotted image",
description: "Alert with slotted image",
type: "warning",
iconSlot: html`<span slot="icon" class="image"
><img src="https://www.home-assistant.io/images/home-assistant-logo.svg"
/></span>`,
},
{
title: "Slotted action",
@@ -106,7 +117,7 @@ const alerts: {
title: "Error with action",
description: "This is a test error alert with action (RTL)",
type: "error",
action: "restart",
actionSlot: html`<mwc-button slot="action" label="restart"></mwc-button>`,
rtl: true,
},
{
@@ -121,30 +132,60 @@ const alerts: {
export class DemoHaAlert extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="ha-alert demo">
<div class="card-content">
${alerts.map(
(alert) => html`
<ha-alert
.title=${alert.title || ""}
.alertType=${alert.type}
.dismissable=${alert.dismissable || false}
.actionText=${alert.action || ""}
.rtl=${alert.rtl || false}
>
${alert.iconSlot} ${alert.description} ${alert.actionSlot}
</ha-alert>
`
)}
</div>
</ha-card>
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-alert ${mode} demo">
<div class="card-content">
${alerts.map(
(alert) => html`
<ha-alert
.title=${alert.title || ""}
.alertType=${alert.type}
.dismissable=${alert.dismissable || false}
.rtl=${alert.rtl || false}
>
${alert.iconSlot} ${alert.description} ${alert.actionSlot}
</ha-alert>
`
)}
</div>
</ha-card>
</div>
`
)}
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
}
static get styles() {
return css`
:host {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 0 50px;
}
ha-card {
max-width: 600px;
margin: 24px auto;
}
ha-alert {
@@ -157,14 +198,14 @@ export class DemoHaAlert extends LitElement {
align-items: center;
justify-content: space-between;
}
span {
margin-right: 16px;
.image {
display: inline-flex;
height: 100%;
align-items: center;
}
ha-logo-svg {
width: 28px;
height: 28px;
padding-right: 8px;
place-self: center;
img {
max-height: 24px;
width: 24px;
}
mwc-button {
--mdc-theme-primary: var(--primary-text-color);
@@ -3,6 +3,7 @@ import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../src/components/ha-card";
import "../../../src/components/ha-chip";
import "../../../src/components/ha-chip-set";
import "../../../src/components/ha-svg-icon";
const chips: {
@@ -22,8 +23,8 @@ const chips: {
},
];
@customElement("demo-ha-chip")
export class DemoHaChip extends LitElement {
@customElement("demo-ha-chips")
export class DemoHaChips extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="ha-chip demo">
@@ -41,6 +42,23 @@ export class DemoHaChip extends LitElement {
)}
</div>
</ha-card>
<ha-card header="ha-chip-set demo">
<div class="card-content">
<ha-chip-set>
${chips.map(
(chip) => html`
<ha-chip .hasIcon=${chip.icon !== undefined}>
${chip.icon
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
</ha-svg-icon>`
: ""}
${chip.content}
</ha-chip>
`
)}
</ha-chip-set>
</div>
</ha-card>
`;
}
@@ -50,12 +68,19 @@ export class DemoHaChip extends LitElement {
max-width: 600px;
margin: 24px auto;
}
ha-chip {
margin-bottom: 4px;
}
.card-content {
display: flex;
flex-direction: column;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-chip": DemoHaChip;
"demo-ha-chips": DemoHaChips;
}
}
-5
View File
@@ -176,11 +176,6 @@ class HaGallery extends PolymerElement {
this.addEventListener("alert-dismissed-clicked", () =>
this.$.notifications.showDialog({ message: "Alert dismissed clicked" })
);
this.addEventListener("alert-action-clicked", () =>
this.$.notifications.showDialog({ message: "Alert action clicked" })
);
this.addEventListener("hass-more-info", (ev) => {
if (ev.detail.entityId) {
this.$.notifications.showDialog({
@@ -4,7 +4,7 @@ import "../../../../src/components/ha-circular-progress";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { HomeAssistant, Route } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import "./hassio-addon-info";
@@ -12,6 +12,8 @@ import "./hassio-addon-info";
class HassioAddonInfoDashboard extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@@ -27,6 +29,7 @@ class HassioAddonInfoDashboard extends LitElement {
<div class="content">
<hassio-addon-info
.narrow=${this.narrow}
.route=${this.route}
.hass=${this.hass}
.supervisor=${this.supervisor}
.addon=${this.addon}
+22 -22
View File
@@ -62,12 +62,13 @@ import {
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { HomeAssistant, Route } from "../../../../src/types";
import { bytesToString } from "../../../../src/util/bytes-to-string";
import "../../components/hassio-card-content";
import "../../components/supervisor-metric";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { hassioStyle } from "../../resources/hassio-style";
import "../../update-available/update-available-card";
import { addonArchIsSupported, extractChangelog } from "../../util/addon";
const STAGE_ICON = {
@@ -89,6 +90,8 @@ const RATING_ICON = {
class HassioAddonInfo extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@@ -125,23 +128,12 @@ class HassioAddonInfo extends LitElement {
return html`
${this.addon.update_available
? html`
<ha-alert
.title=${this.supervisor.localize("common.update_available", {
count: 1,
})}
>
${this.supervisor.localize(
"addon.dashboard.new_update_available",
{ name: this.addon.name, version: this.addon.version_latest }
)}
<a
href="/hassio/update-available/${this.addon.slug}"
slot="action"
>
<mwc-button .label=${this.supervisor.localize("common.review")}>
</mwc-button>
</a>
</ha-alert>
<update-available-card
.hass=${this.hass}
.narrow=${this.narrow}
.supervisor=${this.supervisor}
.addonSlug=${this.addon.slug}
></update-available-card>
`
: ""}
${!this.addon.protected
@@ -151,14 +143,18 @@ class HassioAddonInfo extends LitElement {
.title=${this.supervisor.localize(
"addon.dashboard.protection_mode.title"
)}
.actionText=${this.supervisor.localize(
"addon.dashboard.protection_mode.enable"
)}
@alert-action-clicked=${this._protectionToggled}
>
${this.supervisor.localize(
"addon.dashboard.protection_mode.content"
)}
<mwc-button
slot="action"
.label=${this.supervisor.localize(
"addon.dashboard.protection_mode.enable"
)}
@click=${this._protectionToggled}
>
</mwc-button>
</ha-alert>
`
: ""}
@@ -1167,6 +1163,10 @@ class HassioAddonInfo extends LitElement {
text-decoration: none;
}
update-available-card {
padding-bottom: 16px;
}
@media (max-width: 720px) {
ha-chip {
line-height: 36px;
+1 -1
View File
@@ -111,7 +111,7 @@ export class HassioUpdate extends LitElement {
</div>
<div class="card-actions">
<a href="/hassio/update-available/${key}">
<mwc-button .label=${this.supervisor.localize("common.review")}>
<mwc-button .label=${this.supervisor.localize("common.show")}>
</mwc-button>
</a>
</div>
+1 -3
View File
@@ -10,7 +10,7 @@ import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import "../../src/layouts/hass-loading-screen";
import { HomeAssistant, Route } from "../../src/types";
import { HomeAssistant } from "../../src/types";
import "./hassio-router";
import { SupervisorBaseElement } from "./supervisor-base-element";
@@ -24,8 +24,6 @@ export class HassioMain extends SupervisorBaseElement {
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route?: Route;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
+6 -2
View File
@@ -25,7 +25,7 @@ import {
} from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import { HomeAssistant } from "../../src/types";
import { HomeAssistant, Route } from "../../src/types";
import { getTranslation } from "../../src/util/common-translation";
declare global {
@@ -38,6 +38,8 @@ declare global {
export class SupervisorBaseElement extends urlSyncMixin(
ProvideHassLitMixin(LitElement)
) {
@property({ attribute: false }) public route?: Route;
@property({ attribute: false }) public supervisor: Partial<Supervisor> = {
localize: () => "",
};
@@ -108,7 +110,9 @@ export class SupervisorBaseElement extends urlSyncMixin(
this._language = this.hass.language;
}
this._initializeLocalize();
this._initSupervisor();
if (this.route?.prefix === "/hassio") {
this._initSupervisor();
}
}
private async _initializeLocalize() {
+1 -1
View File
@@ -71,7 +71,7 @@ class HassioCoreInfo extends LitElement {
? html`
<a href="/hassio/update-available/core">
<mwc-button
.label=${this.supervisor.localize("common.review")}
.label=${this.supervisor.localize("common.show")}
>
</mwc-button>
</a>
+1 -1
View File
@@ -110,7 +110,7 @@ class HassioHostInfo extends LitElement {
? html`
<a href="/hassio/update-available/os">
<mwc-button
.label=${this.supervisor.localize("common.review")}
.label=${this.supervisor.localize("common.show")}
>
</mwc-button>
</a>
+18 -11
View File
@@ -81,7 +81,7 @@ class HassioSupervisorInfo extends LitElement {
? html`
<a href="/hassio/update-available/supervisor">
<mwc-button
.label=${this.supervisor.localize("common.review")}
.label=${this.supervisor.localize("common.show")}
>
</mwc-button>
</a>
@@ -151,24 +151,28 @@ class HassioSupervisorInfo extends LitElement {
></ha-switch>
</ha-settings-row>`
: ""
: html`<ha-alert
alert-type="warning"
.actionText=${this.supervisor.localize("common.learn_more")}
@alert-action-clicked=${this._unsupportedDialog}
>
: html`<ha-alert alert-type="warning">
${this.supervisor.localize(
"system.supervisor.unsupported_title"
)}
<mwc-button
slot="action"
.label=${this.supervisor.localize("common.learn_more")}
@click=${this._unsupportedDialog}
>
</mwc-button>
</ha-alert>`}
${!this.supervisor.supervisor.healthy
? html`<ha-alert
alert-type="error"
.actionText=${this.supervisor.localize("common.learn_more")}
@alert-action-clicked=${this._unhealthyDialog}
>
? html`<ha-alert alert-type="error">
${this.supervisor.localize(
"system.supervisor.unhealthy_title"
)}
<mwc-button
slot="action"
.label=${this.supervisor.localize("common.learn_more")}
@click=${this._unhealthyDialog}
>
</mwc-button>
</ha-alert>`
: ""}
</div>
@@ -466,6 +470,9 @@ class HassioSupervisorInfo extends LitElement {
white-space: normal;
color: var(--secondary-text-color);
}
ha-alert mwc-button {
--mdc-theme-primary: var(--primary-text-color);
}
a {
text-decoration: none;
}
@@ -0,0 +1,401 @@
import "@material/mwc-list/mwc-list-item";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/common/search/search-input";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-expansion-panel";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-markdown";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon";
import "../../../src/components/ha-switch";
import {
fetchHassioAddonChangelog,
fetchHassioAddonInfo,
HassioAddonDetails,
updateHassioAddon,
} from "../../../src/data/hassio/addon";
import {
createHassioPartialBackup,
HassioPartialBackupCreateParams,
} from "../../../src/data/hassio/backup";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { updateOS } from "../../../src/data/hassio/host";
import { updateSupervisor } from "../../../src/data/hassio/supervisor";
import { updateCore } from "../../../src/data/supervisor/core";
import { StoreAddon } from "../../../src/data/supervisor/store";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage";
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
import { HomeAssistant, Route } from "../../../src/types";
import { documentationUrl } from "../../../src/util/documentation-url";
import { addonArchIsSupported, extractChangelog } from "../util/addon";
declare global {
interface HASSDomEvents {
"update-complete": undefined;
}
}
type updateType = "os" | "supervisor" | "core" | "addon";
const changelogUrl = (
hass: HomeAssistant,
entry: updateType,
version: string
): string | undefined => {
if (entry === "addon") {
return undefined;
}
if (entry === "core") {
return version?.includes("dev")
? "https://github.com/home-assistant/core/commits/dev"
: documentationUrl(hass, "/latest-release-notes/");
}
if (entry === "os") {
return version?.includes("dev")
? "https://github.com/home-assistant/operating-system/commits/dev"
: `https://github.com/home-assistant/operating-system/releases/tag/${version}`;
}
if (entry === "supervisor") {
return version?.includes("dev")
? "https://github.com/home-assistant/supervisor/commits/main"
: `https://github.com/home-assistant/supervisor/releases/tag/${version}`;
}
return undefined;
};
@customElement("update-available-card")
class UpdateAvailableCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public addonSlug?: string;
@state() private _updateType?: updateType;
@state() private _changelogContent?: string;
@state() private _addonInfo?: HassioAddonDetails;
@state() private _action: "backup" | "update" | null = null;
@state() private _error?: string;
private _addonStoreInfo = memoizeOne(
(slug: string, storeAddons: StoreAddon[]) =>
storeAddons.find((addon) => addon.slug === slug)
);
protected render(): TemplateResult {
if (
!this._updateType ||
(this._updateType === "addon" && !this._addonInfo)
) {
return html``;
}
const changelog = changelogUrl(this.hass, this._updateType, this._version);
return html`
<ha-card
.header=${this.supervisor.localize("update_available.update_name", {
name: this._name,
})}
>
<div class="card-content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${this._action === null
? html`
${this._changelogContent
? html`
<ha-expansion-panel header="Changelog" outlined>
<ha-markdown .content=${this._changelogContent}>
</ha-markdown>
</ha-expansion-panel>
`
: ""}
<div class="versions">
<p>
${this.supervisor.localize("update_available.description", {
name: this._name,
version: this._version,
newest_version: this._version_latest,
})}
</p>
</div>
${["core", "addon"].includes(this._updateType)
? html`
<ha-formfield
.label=${this.supervisor.localize(
"update_available.create_backup"
)}
>
<ha-checkbox checked></ha-checkbox>
</ha-formfield>
`
: ""}
`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? this.supervisor.localize("update_available.updating", {
name: this._name,
version: this._version_latest,
})
: this.supervisor.localize(
"update_available.creating_backup",
{ name: this._name }
)}
</p>`}
</div>
${this._action === null
? html`
<div class="card-actions">
${changelog
? html`<a .href=${changelog} target="_blank" rel="noreferrer">
<mwc-button
.label=${this.supervisor.localize(
"update_available.open_release_notes"
)}
>
</mwc-button>
</a>`
: ""}
<span></span>
<ha-progress-button
.disabled=${!this._version ||
(this._shouldCreateBackup &&
this.supervisor.info.state !== "running")}
@click=${this._update}
raised
>
${this.supervisor.localize("common.update")}
</ha-progress-button>
</div>
`
: ""}
</ha-card>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
const pathPart = this.route?.path.substring(1, this.route.path.length);
const updateType = ["core", "os", "supervisor"].includes(pathPart)
? pathPart
: "addon";
this._updateType = updateType as updateType;
if (updateType === "addon") {
if (!this.addonSlug) {
this.addonSlug = pathPart;
}
this._loadAddonData();
}
}
get _shouldCreateBackup(): boolean {
return this.shadowRoot?.querySelector("ha-checkbox")?.checked || true;
}
get _version(): string {
return this._updateType
? this._updateType === "addon"
? this._addonInfo!.version
: this.supervisor[this._updateType]?.version || ""
: "";
}
get _version_latest(): string {
return this._updateType
? this._updateType === "addon"
? this._addonInfo!.version_latest
: this.supervisor[this._updateType]?.version_latest || ""
: "";
}
get _name(): string {
return this._updateType
? this._updateType === "addon"
? this._addonInfo!.name
: SUPERVISOR_UPDATE_NAMES[this._updateType]
: "";
}
private async _loadAddonData() {
try {
this._addonInfo = await fetchHassioAddonInfo(this.hass, this.addonSlug!);
} catch (err) {
showAlertDialog(this, {
title: this._updateType,
text: extractApiErrorMessage(err),
});
return;
}
const addonStoreInfo =
!this._addonInfo.detached && !this._addonInfo.available
? this._addonStoreInfo(
this._addonInfo.slug,
this.supervisor.store.addons
)
: undefined;
if (this._addonInfo.changelog) {
try {
const content = await fetchHassioAddonChangelog(
this.hass,
this.addonSlug!
);
this._changelogContent = extractChangelog(this._addonInfo, content);
} catch (err) {
this._error = extractApiErrorMessage(err);
return;
}
}
if (!this._addonInfo.available && addonStoreInfo) {
if (
!addonArchIsSupported(
this.supervisor.info.supported_arch,
this._addonInfo.arch
)
) {
this._error = this.supervisor.localize(
"addon.dashboard.not_available_arch"
);
} else {
this._error = this.supervisor.localize(
"addon.dashboard.not_available_version",
{
core_version_installed: this.supervisor.core.version,
core_version_needed: addonStoreInfo.homeassistant,
}
);
}
}
}
private async _update() {
this._error = undefined;
if (this._shouldCreateBackup) {
let backupArgs: HassioPartialBackupCreateParams;
if (this._updateType === "addon") {
backupArgs = {
name: `addon_${this.addonSlug}_${this._version}`,
addons: [this.addonSlug!],
homeassistant: false,
};
} else {
backupArgs = {
name: `${this._updateType}_${this._version}`,
folders: ["homeassistant"],
homeassistant: true,
};
}
this._action = "backup";
try {
await createHassioPartialBackup(this.hass, backupArgs);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try {
if (this._updateType === "addon") {
await updateHassioAddon(this.hass, this.addonSlug!);
} else if (this._updateType === "core") {
await updateCore(this.hass);
} else if (this._updateType === "os") {
await updateOS(this.hass);
} else if (this._updateType === "supervisor") {
await updateSupervisor(this.hass);
}
} catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
fireEvent(this, "update-complete");
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
}
ha-card {
margin: auto;
}
a {
text-decoration: none;
color: var(--primary-text-color);
}
ha-settings-row {
padding: 0;
}
.card-actions {
display: flex;
justify-content: space-between;
border-top: none;
padding: 0 8px 8px;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
ha-markdown {
padding-bottom: 8px;
}
ha-formfield {
cursor: pointer;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"update-available-card": UpdateAvailableCard;
}
}
@@ -1,77 +1,11 @@
import "@material/mwc-list/mwc-list-item";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import "../../../src/common/search/search-input";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-expansion-panel";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-markdown";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon";
import "../../../src/components/ha-switch";
import {
fetchHassioAddonChangelog,
fetchHassioAddonInfo,
HassioAddonDetails,
updateHassioAddon,
} from "../../../src/data/hassio/addon";
import {
createHassioPartialBackup,
HassioPartialBackupCreateParams,
} from "../../../src/data/hassio/backup";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { updateOS } from "../../../src/data/hassio/host";
import { updateSupervisor } from "../../../src/data/hassio/supervisor";
import { updateCore } from "../../../src/data/supervisor/core";
import { StoreAddon } from "../../../src/data/supervisor/store";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage";
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
import { HomeAssistant, Route } from "../../../src/types";
import { documentationUrl } from "../../../src/util/documentation-url";
import { addonArchIsSupported, extractChangelog } from "../util/addon";
const changelogUrl = (
hass: HomeAssistant,
entry: string,
version: string
): string | undefined => {
if (entry === "core") {
return version?.includes("dev")
? "https://github.com/home-assistant/core/commits/dev"
: documentationUrl(hass, "/latest-release-notes/");
}
if (entry === "os") {
return version?.includes("dev")
? "https://github.com/home-assistant/operating-system/commits/dev"
: `https://github.com/home-assistant/operating-system/releases/tag/${version}`;
}
if (entry === "supervisor") {
return version?.includes("dev")
? "https://github.com/home-assistant/supervisor/commits/main"
: `https://github.com/home-assistant/supervisor/releases/tag/${version}`;
}
return undefined;
};
import "./update-available-card";
@customElement("update-available-dashboard")
class UpdateAvailableDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -81,311 +15,45 @@ class UpdateAvailableDashboard extends LitElement {
@property({ attribute: false }) public route!: Route;
@state() private _updateEntry?: string;
@state() private _changelogContent?: string;
@state() private _addonInfo?: HassioAddonDetails;
@state() private _createBackup = true;
@state() private _action: "backup" | "update" | null = null;
@state() private _error?: string;
private _isAddon = false;
private _addonStoreInfo = memoizeOne(
(slug: string, storeAddons: StoreAddon[]) =>
storeAddons.find((addon) => addon.slug === slug)
);
protected render(): TemplateResult {
if (!this._updateEntry) {
return html``;
}
const name =
// @ts-ignore
this._addonInfo?.name || SUPERVISOR_UPDATE_NAMES[this._updateEntry];
const changelog = !this._isAddon
? changelogUrl(
this.hass,
this._updateEntry,
this.supervisor[this._updateEntry]?.version
)
: undefined;
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
>
<ha-card
.header=${this.supervisor.localize("update_available.update_name", {
name,
})}
>
<div class="card-content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${this._action === null
? html`
${this._changelogContent
? html`
<ha-expansion-panel header="Changelog" outlined>
<ha-markdown .content=${this._changelogContent}>
</ha-markdown>
</ha-expansion-panel>
`
: ""}
<div class="versions">
<p>
${this.supervisor.localize(
"update_available.description",
{
name,
version:
this._addonInfo?.version ||
this.supervisor[this._updateEntry]?.version,
newest_version:
this._addonInfo?.version_latest ||
this.supervisor[this._updateEntry]?.version_latest,
}
)}
</p>
${this._updateEntry === "core"
? html`
<i>
${this.supervisor.localize(
"update_available.core_note",
{
version:
this._addonInfo?.version ||
this.supervisor[this._updateEntry]?.version,
}
)}
</i>
`
: ""}
</div>
${!["os", "supervisor"].includes(this._updateEntry)
? html`
<ha-settings-row>
<ha-checkbox
slot="prefix"
.checked=${this._createBackup}
@click=${this._toggleBackup}
>
</ha-checkbox>
<span slot="heading">
${this.supervisor.localize(
"update_available.create_backup"
)}
</span>
</ha-settings-row>
`
: ""}
`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? this.supervisor.localize("update_available.updating", {
name,
version:
this._addonInfo?.version_latest ||
this.supervisor[this._updateEntry]?.version_latest,
})
: this.supervisor.localize(
"update_available.creating_backup",
{ name }
)}
</p>`}
</div>
${this._action === null
? html`
<div class="card-actions">
${changelog
? html`<a
.href=${changelog}
target="_blank"
rel="noreferrer"
>
<mwc-button
.label=${this.supervisor.localize(
"update_available.open_release_notes"
)}
>
</mwc-button>
</a>`
: ""}
<span></span>
<ha-progress-button
.disabled=${this._error !== undefined}
@click=${this._update}
raised
>
${this.supervisor.localize("common.update")}
</ha-progress-button>
</div>
`
: ""}
</ha-card>
<update-available-card
.hass=${this.hass}
.supervisor=${this.supervisor}
.route=${this.route}
.narrow=${this.narrow}
@update-complete=${this._updateComplete}
></update-available-card>
</hass-subpage>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._updateEntry = this.route.path.substring(1, this.route.path.length);
this._isAddon = !["core", "os", "supervisor"].includes(this._updateEntry);
if (this._isAddon) {
this._loadAddonData();
}
}
private async _loadAddonData() {
try {
this._addonInfo = await fetchHassioAddonInfo(
this.hass,
this._updateEntry!
);
} catch (err) {
showAlertDialog(this, {
title: this._updateEntry,
text: extractApiErrorMessage(err),
confirm: () => history.back(),
});
return;
}
const addonStoreInfo =
!this._addonInfo.detached && !this._addonInfo.available
? this._addonStoreInfo(
this._addonInfo.slug,
this.supervisor.store.addons
)
: undefined;
if (this._addonInfo.changelog) {
try {
const content = await fetchHassioAddonChangelog(
this.hass,
this._updateEntry!
);
this._changelogContent = extractChangelog(this._addonInfo, content);
} catch (err) {
this._error = extractApiErrorMessage(err);
return;
}
}
if (!this._addonInfo.available && addonStoreInfo) {
if (
!addonArchIsSupported(
this.supervisor.info.supported_arch,
this._addonInfo.arch
)
) {
this._error = this.supervisor.localize(
"addon.dashboard.not_available_arch"
);
} else {
this._error = this.supervisor.localize(
"addon.dashboard.not_available_arch",
{
core_version_installed: this.supervisor.core.version,
core_version_needed: addonStoreInfo.homeassistant,
}
);
}
}
}
private _toggleBackup() {
this._createBackup = !this._createBackup;
}
private async _update() {
if (this._createBackup) {
let backupArgs: HassioPartialBackupCreateParams;
if (this._isAddon) {
backupArgs = {
name: `addon_${this._updateEntry}_${this._addonInfo?.version}`,
addons: [this._updateEntry!],
homeassistant: false,
};
} else {
backupArgs = {
name: `${this._updateEntry}_${this._addonInfo?.version}`,
folders: ["homeassistant"],
homeassistant: true,
};
}
this._action = "backup";
try {
await createHassioPartialBackup(this.hass, backupArgs);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try {
if (this._isAddon) {
await updateHassioAddon(this.hass, this._updateEntry!);
} else if (this._updateEntry === "core") {
await updateCore(this.hass);
} else if (this._updateEntry === "os") {
await updateOS(this.hass);
} else if (this._updateEntry === "supervisor") {
await updateSupervisor(this.hass);
}
} catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
private _updateComplete() {
history.back();
}
static get styles(): CSSResultGroup {
return css`
hass-subpage {
--app-header-background-color: background-color: var(--primary-background-color);
}
ha-card {
hass-subpage {
--app-header-background-color: var(--primary-background-color);
--app-header-text-color: var(--sidebar-text-color);
}
update-available-card {
margin: auto;
margin-top: 16px;
max-width: 600px;
}
a {
text-decoration: none;
color: var(--primary-text-color);
}
ha-settings-row {
padding: 0;
}
.card-actions {
display: flex;
justify-content: space-between;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
`;
}
}
customElements.define("update-available-dashboard", UpdateAvailableDashboard);
declare global {
interface HTMLElementTagNameMap {
"update-available-dashboard": UpdateAvailableDashboard;
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20211117.0",
version="20211123.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/frontend",
author="The Home Assistant Authors",
+16
View File
@@ -11,4 +11,20 @@ export interface ReceiverStatusMessage extends BaseCastMessage {
urlPath?: string | null;
}
export interface ReceiverErrorMessage extends BaseCastMessage {
type: "receiver_error";
error_code: ReceiverErrorCode;
error_message: string;
}
export const enum ReceiverErrorCode {
CONNECTION_FAILED = 1,
AUTHENTICATION_FAILED = 2,
CONNECTION_LOST = 3,
HASS_URL_MISSING = 4,
NO_HTTPS = 5,
NOT_CONNECTED = 21,
FETCH_CONFIG_FAILED = 22,
}
export type SenderMessage = ReceiverStatusMessage;
+7 -1
View File
@@ -7,7 +7,13 @@ export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
!hideAdvancedPage(hass, page);
const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) =>
!page.component || isComponentLoaded(hass, page.component);
page.component
? isComponentLoaded(hass, page.component)
: page.components
? page.components.some((integration) =>
isComponentLoaded(hass, integration)
)
: true;
const isCore = (page: PageNavigation) => page.core;
const isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced;
+1
View File
@@ -119,6 +119,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
current: mdiCurrentAc,
date: mdiCalendar,
energy: mdiLightningBolt,
frequency: mdiSineWave,
gas: mdiGasCylinder,
humidity: mdiWaterPercent,
illuminance: mdiBrightness5,
+29 -38
View File
@@ -1,4 +1,3 @@
import "@material/mwc-button/mwc-button";
import {
mdiAlertCircleOutline,
mdiAlertOutline,
@@ -23,7 +22,6 @@ const ALERT_ICONS = {
declare global {
interface HASSDomEvents {
"alert-dismissed-clicked": undefined;
"alert-action-clicked": undefined;
}
}
@@ -37,8 +35,6 @@ class HaAlert extends LitElement {
| "error"
| "success" = "info";
@property({ attribute: "action-text" }) public actionText = "";
@property({ type: Boolean }) public dismissable = false;
@property({ type: Boolean }) public rtl = false;
@@ -51,11 +47,11 @@ class HaAlert extends LitElement {
[this.alertType]: true,
})}"
>
<slot name="icon">
<div class="icon ${this.title ? "" : "no-title"}">
<div class="icon ${this.title ? "" : "no-title"}">
<slot name="icon">
<ha-svg-icon .path=${ALERT_ICONS[this.alertType]}></ha-svg-icon>
</div>
</slot>
</slot>
</div>
<div class="content">
<div class="main-content">
${this.title ? html`<div class="title">${this.title}</div>` : ""}
@@ -63,12 +59,7 @@ class HaAlert extends LitElement {
</div>
<div class="action">
<slot name="action">
${this.actionText
? html`<mwc-button
@click=${this._action_clicked}
.label=${this.actionText}
></mwc-button>`
: this.dismissable
${this.dismissable
? html`<ha-icon-button
@click=${this._dismiss_clicked}
label="Dismiss alert"
@@ -86,10 +77,6 @@ class HaAlert extends LitElement {
fireEvent(this, "alert-dismissed-clicked");
}
private _action_clicked() {
fireEvent(this, "alert-action-clicked");
}
static styles = css`
.issue-type {
position: relative;
@@ -100,7 +87,7 @@ class HaAlert extends LitElement {
.issue-type.rtl {
flex-direction: row-reverse;
}
.issue-type::before {
.issue-type::after {
position: absolute;
top: 0;
right: 0;
@@ -111,18 +98,12 @@ class HaAlert extends LitElement {
content: "";
border-radius: 4px;
}
slot > .icon {
margin-right: 8px;
width: 24px;
.icon {
z-index: 1;
}
.icon.no-title {
align-self: center;
}
.issue-type.rtl > slot > .icon {
margin-right: 0px;
margin-left: 8px;
width: 24px;
}
.issue-type.rtl > .content {
flex-direction: row-reverse;
text-align: right;
@@ -133,44 +114,54 @@ class HaAlert extends LitElement {
align-items: center;
width: 100%;
}
.action {
z-index: 1;
width: min-content;
--mdc-theme-primary: var(--primary-text-color);
}
.main-content {
overflow-wrap: anywhere;
margin-left: 8px;
margin-right: 0;
}
.issue-type.rtl > .content > .main-content {
margin-left: 0;
margin-right: 8px;
}
.title {
margin-top: 2px;
font-weight: bold;
}
mwc-button {
.action mwc-button,
.action ha-icon-button {
--mdc-theme-primary: var(--primary-text-color);
}
ha-icon-button {
--mdc-icon-button-size: 36px;
}
.issue-type.info > slot > .icon {
.issue-type.info > .icon {
color: var(--info-color);
}
.issue-type.info::before {
.issue-type.info::after {
background-color: var(--info-color);
}
.issue-type.warning > slot > .icon {
.issue-type.warning > .icon {
color: var(--warning-color);
}
.issue-type.warning::before {
.issue-type.warning::after {
background-color: var(--warning-color);
}
.issue-type.error > slot > .icon {
.issue-type.error > .icon {
color: var(--error-color);
}
.issue-type.error::before {
.issue-type.error::after {
background-color: var(--error-color);
}
.issue-type.success > slot > .icon {
.issue-type.success > .icon {
color: var(--success-color);
}
.issue-type.success::before {
.issue-type.success::after {
background-color: var(--success-color);
}
`;
+9 -30
View File
@@ -8,52 +8,31 @@ import {
TemplateResult,
unsafeCSS,
} from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-chip";
declare global {
// for fire event
interface HASSDomEvents {
"chip-clicked": { index: string };
}
}
import { customElement } from "lit/decorators";
@customElement("ha-chip-set")
export class HaChipSet extends LitElement {
@property() public items = [];
protected render(): TemplateResult {
if (this.items.length === 0) {
return html``;
}
return html`
<div class="mdc-chip-set">
${this.items.map(
(item, idx) =>
html`
<ha-chip .index=${idx} @click=${this._handleClick}>
${item}
</ha-chip>
`
)}
<slot></slot>
</div>
`;
}
private _handleClick(ev): void {
fireEvent(this, "chip-clicked", {
index: ev.currentTarget.index,
});
}
static get styles(): CSSResultGroup {
return css`
${unsafeCSS(chipStyles)}
ha-chip {
slot::slotted(ha-chip) {
margin: 4px;
}
slot::slotted(ha-chip:first-of-type) {
margin-left: -4px;
}
slot::slotted(ha-chip:last-of-type) {
margin-right: -4px;
}
`;
}
}
+1 -10
View File
@@ -10,22 +10,13 @@ import {
} from "lit";
import { customElement, property } from "lit/decorators";
declare global {
// for fire event
interface HASSDomEvents {
"chip-clicked": { index: string };
}
}
@customElement("ha-chip")
export class HaChip extends LitElement {
@property() public index = 0;
@property({ type: Boolean }) public hasIcon = false;
protected render(): TemplateResult {
return html`
<div class="mdc-chip" .index=${this.index}>
<div class="mdc-chip">
${this.hasIcon
? html`<div class="mdc-chip__icon mdc-chip__icon--leading">
<slot name="icon"></slot>
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
import { startOfYesterday } from "date-fns";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
@@ -22,6 +23,8 @@ export class MoreInfoHistory extends LitElement {
@state() private _stateHistory?: HistoryResult;
private _showMoreHref = "";
private _throttleGetStateHistory = throttle(() => {
this._getStateHistory();
}, 10000);
@@ -31,14 +34,12 @@ export class MoreInfoHistory extends LitElement {
return html``;
}
const href = "/history?entity_id=" + this.entityId;
return html`${isComponentLoaded(this.hass, "history")
? html` <div class="header">
<div class="title">
${this.hass.localize("ui.dialogs.more_info_control.history")}
</div>
<a href=${href} @click=${this._close}
<a href=${this._showMoreHref} @click=${this._close}
>${this.hass.localize(
"ui.dialogs.more_info_control.show_more"
)}</a
@@ -63,6 +64,10 @@ export class MoreInfoHistory extends LitElement {
return;
}
this._showMoreHref = `/history?entity_id=${
this.entityId
}&start_date=${startOfYesterday().toISOString()}`;
this._throttleGetStateHistory();
return;
}
@@ -1,3 +1,4 @@
import { startOfYesterday } from "date-fns";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
@@ -30,6 +31,8 @@ export class MoreInfoLogbook extends LitElement {
private _error?: string;
private _showMoreHref = "";
private _throttleGetLogbookEntries = throttle(() => {
this._getLogBookData();
}, 10000);
@@ -44,8 +47,6 @@ export class MoreInfoLogbook extends LitElement {
return html``;
}
const href = "/logbook?entity_id=" + this.entityId;
return html`
${isComponentLoaded(this.hass, "logbook")
? this._error
@@ -67,7 +68,7 @@ export class MoreInfoLogbook extends LitElement {
<div class="title">
${this.hass.localize("ui.dialogs.more_info_control.logbook")}
</div>
<a href=${href} @click=${this._close}
<a href=${this._showMoreHref} @click=${this._close}
>${this.hass.localize(
"ui.dialogs.more_info_control.show_more"
)}</a
@@ -106,6 +107,10 @@ export class MoreInfoLogbook extends LitElement {
return;
}
this._showMoreHref = `/logbook?entity_id=${
this.entityId
}&start_date=${startOfYesterday().toISOString()}`;
this._throttleGetLogbookEntries();
return;
}
File diff suppressed because one or more lines are too long
+14
View File
@@ -13,6 +13,8 @@ class HassSubpage extends LitElement {
@property({ type: Boolean, attribute: "main-page" }) public mainPage = false;
@property({ type: String, attribute: "back-path" }) public backPath?: string;
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ type: Boolean }) public supervisor = false;
@@ -31,6 +33,14 @@ class HassSubpage extends LitElement {
.narrow=${this.narrow}
></ha-menu-button>
`
: this.backPath
? html`
<a href=${this.backPath}>
<ha-icon-button-arrow-prev
.hass=${this.hass}
></ha-icon-button-arrow-prev>
</a>
`
: html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
@@ -80,6 +90,10 @@ class HassSubpage extends LitElement {
border-bottom: var(--app-header-border-bottom, none);
box-sizing: border-box;
}
.toolbar a {
color: var(--app-header-text-color);
text-decoration: none;
}
ha-menu-button,
ha-icon-button-arrow-prev,
+5 -2
View File
@@ -24,10 +24,13 @@ export interface PageNavigation {
path: string;
translationKey?: string;
component?: string;
components?: string[];
name?: string;
core?: boolean;
advancedOnly?: boolean;
iconPath?: string;
description?: string;
iconColor?: string;
info?: any;
}
@@ -85,7 +88,7 @@ class HassTabsSubpage extends LitElement {
<a href=${page.path}>
<ha-tab
.hass=${this.hass}
.active=${page === activeTab}
.active=${page.path === activeTab?.path}
.narrow=${this.narrow}
.name=${page.translationKey
? localizeFunc(page.translationKey)
@@ -224,7 +227,7 @@ class HassTabsSubpage extends LitElement {
box-sizing: border-box;
}
.toolbar a {
color: var(--sidebar-text-color);
color: var(--app-header-text-color);
text-decoration: none;
}
.bottom-bar a {
@@ -135,7 +135,7 @@ class HaConfigAreaPage extends LitElement {
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.tabs=${configSections.integrations}
.tabs=${configSections.devices}
.route=${this.route}
>
${this.narrow
@@ -89,7 +89,7 @@ export class HaConfigAreasDashboard extends LitElement {
.narrow=${this.narrow}
.isWide=${this.isWide}
back-path="/config"
.tabs=${configSections.integrations}
.tabs=${configSections.devices}
.route=${this.route}
>
<ha-icon-button
@@ -114,7 +114,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
.narrow=${this.narrow}
.route=${this.route}
.backCallback=${this._backTapped}
.tabs=${configSections.automation}
.tabs=${configSections.automations}
>
<ha-button-menu
corner="BOTTOM_START"
@@ -225,7 +225,7 @@ class HaAutomationPicker extends LitElement {
back-path="/config"
id="entity_id"
.route=${this.route}
.tabs=${configSections.automation}
.tabs=${configSections.automations}
.activeFilters=${this._activeFilters}
.columns=${this._columns(this.narrow, this.hass.locale)}
.data=${this._automations(this.automations, this._filteredAutomations)}
@@ -112,7 +112,7 @@ export class HaAutomationTrace extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${configSections.automation}
.tabs=${configSections.automations}
>
${this.narrow
? html`<span slot="header">${title}</span>
@@ -224,7 +224,7 @@ class HaBlueprintOverview extends LitElement {
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.automation}
.tabs=${configSections.automations}
.columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._processedBlueprints(this.blueprints)}
id="entity_id"
+1 -1
View File
@@ -50,7 +50,7 @@ export class CloudLogin extends LitElement {
header="Home Assistant Cloud"
>
<div class="content">
<ha-config-section isWide=${this.isWide}>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">Home Assistant Cloud</span>
<div slot="introduction">
<p>
@@ -15,6 +15,7 @@ import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
import "./ha-config-navigation";
import { SupervisorAvailableUpdates } from "../../../data/supervisor/supervisor";
@customElement("ha-config-dashboard")
class HaConfigDashboard extends LitElement {
@@ -27,73 +28,11 @@ class HaConfigDashboard extends LitElement {
@property() public cloudStatus?: CloudStatus;
@property() public supervisorUpdates?: SupervisorAvailableUpdates[] | null;
@property() public showAdvanced!: boolean;
protected render(): TemplateResult {
const content = html` <ha-config-section
.narrow=${this.narrow}
.isWide=${this.isWide}
>
<div slot="header">${this.hass.localize("ui.panel.config.header")}</div>
<div class="intro" slot="introduction">
${this.hass.localize("ui.panel.config.introduction")}
</div>
${isComponentLoaded(this.hass, "hassio")
? html`<ha-config-updates
.hass=${this.hass}
slot="introduction"
></ha-config-updates>`
: ""}
${this.cloudStatus && isComponentLoaded(this.hass, "cloud")
? html`
<ha-card>
<ha-config-navigation
.hass=${this.hass}
.showAdvanced=${this.showAdvanced}
.pages=${[
{
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: this.cloudStatus,
iconPath: mdiCloudLock,
},
]}
></ha-config-navigation>
</ha-card>
`
: ""}
${Object.values(configSections).map(
(section) => html`
<ha-card>
<ha-config-navigation
.hass=${this.hass}
.showAdvanced=${this.showAdvanced}
.pages=${section}
></ha-config-navigation>
</ha-card>
`
)}
${!this.showAdvanced
? html`
<div class="promo-advanced">
${this.hass.localize("ui.panel.config.advanced_mode.hint_enable")}
<a href="/profile"
>${this.hass.localize(
"ui.panel.config.advanced_mode.link_profile_page"
)}</a
>.
</div>
`
: ""}
</ha-config-section>`;
if (!this.narrow && this.hass.dockedSidebar !== "always_hidden") {
return content;
}
return html`
<ha-app-layout>
<app-header fixed slot="header">
@@ -102,10 +41,72 @@ class HaConfigDashboard extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
<div main-title>${this.hass.localize("panel.config")}</div>
</app-toolbar>
</app-header>
${content}
<ha-config-section
.narrow=${this.narrow}
.isWide=${this.isWide}
full-width
>
${isComponentLoaded(this.hass, "hassio") &&
this.supervisorUpdates === undefined
? html``
: html`${this.supervisorUpdates !== null
? html`<ha-card>
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.supervisorUpdates=${this.supervisorUpdates}
></ha-config-updates>
</ha-card>`
: ""}
<ha-card>
${this.narrow && this.supervisorUpdates !== null
? html`<div class="title">
${this.hass.localize("panel.config")}
</div>`
: ""}
${this.cloudStatus && isComponentLoaded(this.hass, "cloud")
? html`
<ha-config-navigation
.hass=${this.hass}
.showAdvanced=${this.showAdvanced}
.pages=${[
{
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: this.cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
},
]}
></ha-config-navigation>
`
: ""}
<ha-config-navigation
.hass=${this.hass}
.showAdvanced=${this.showAdvanced}
.pages=${configSections.dashboard}
></ha-config-navigation>
</ha-card>
${!this.showAdvanced
? html`
<div class="promo-advanced">
${this.hass.localize(
"ui.panel.config.advanced_mode.hint_enable"
)}
<a href="/profile"
>${this.hass.localize(
"ui.panel.config.advanced_mode.link_profile_page"
)}</a
>.
</div>
`
: ""}`}
</ha-config-section>
</ha-app-layout>
`;
}
@@ -115,16 +116,16 @@ class HaConfigDashboard extends LitElement {
haStyle,
css`
app-header {
--app-header-background-color: var(--primary-background-color);
border-bottom: var(--app-header-border-bottom);
--header-height: 55px;
}
ha-card:last-child {
margin-bottom: 24px;
}
ha-config-section {
margin-top: -12px;
}
:host([narrow]) ha-config-section {
margin-top: -20px;
margin: auto;
margin-top: -32px;
max-width: 600px;
}
ha-card {
overflow: hidden;
@@ -133,6 +134,11 @@ class HaConfigDashboard extends LitElement {
text-decoration: none;
color: var(--primary-text-color);
}
.title {
font-size: 16px;
padding: 16px;
padding-bottom: 0;
}
.promo-advanced {
text-align: center;
color: var(--secondary-text-color);
@@ -141,8 +147,13 @@ class HaConfigDashboard extends LitElement {
.promo-advanced a {
color: var(--secondary-text-color);
}
.intro {
margin-bottom: 24px;
:host([narrow]) ha-card {
background-color: var(--primary-background-color);
box-shadow: unset;
}
:host([narrow]) ha-config-section {
margin-top: -42px;
}
`,
];
@@ -24,10 +24,13 @@ class HaConfigNavigation extends LitElement {
? html`
<a href=${page.path} aria-role="option" tabindex="-1">
<paper-icon-item>
<ha-svg-icon
.path=${page.iconPath}
<div
class=${page.iconColor ? "icon-background" : ""}
slot="item-icon"
></ha-svg-icon>
.style="background-color: ${page.iconColor || "undefined"}"
>
<ha-svg-icon .path=${page.iconPath}></ha-svg-icon>
</div>
<paper-item-body two-line>
${page.name ||
this.hass.localize(
@@ -54,7 +57,8 @@ class HaConfigNavigation extends LitElement {
`
: html`
<div secondary>
${this.hass.localize(
${page.description ||
this.hass.localize(
`ui.panel.config.${page.component}.description`
)}
</div>
@@ -81,6 +85,11 @@ class HaConfigNavigation extends LitElement {
ha-svg-icon,
ha-icon-next {
color: var(--secondary-text-color);
height: 24px;
width: 24px;
}
ha-svg-icon {
padding: 8px;
}
.iron-selected paper-item::before,
a:not(.iron-selected):focus::before {
@@ -102,6 +111,12 @@ class HaConfigNavigation extends LitElement {
.iron-selected:focus paper-item::before {
opacity: 0.2;
}
.icon-background {
border-radius: 50%;
}
.icon-background ha-svg-icon {
color: #fff;
}
`;
}
}
@@ -2,23 +2,13 @@ import "@material/mwc-button/mwc-button";
import { mdiPackageVariant } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-alert";
import "../../../components/ha-logo-svg";
import "../../../components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import {
fetchSupervisorAvailableUpdates,
SupervisorAvailableUpdates,
} from "../../../data/supervisor/supervisor";
import { SupervisorAvailableUpdates } from "../../../data/supervisor/supervisor";
import { buttonLinkStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
export const SUPERVISOR_UPDATE_NAMES = {
@@ -31,85 +21,117 @@ export const SUPERVISOR_UPDATE_NAMES = {
class HaConfigUpdates extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _supervisorUpdates?: SupervisorAvailableUpdates[];
@property({ type: Boolean }) public narrow!: boolean;
@state() private _error?: string;
@property({ attribute: false })
public supervisorUpdates?: SupervisorAvailableUpdates[] | null;
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._loadSupervisorUpdates();
}
@state() private _showAll = false;
protected render(): TemplateResult {
if (!this.supervisorUpdates) {
return html``;
}
const updates =
this._showAll || this.supervisorUpdates.length <= 3
? this.supervisorUpdates
: this.supervisorUpdates.slice(0, 2);
return html`
${this._error
? html`<ha-alert
.title=${this.hass.localize(
"ui.panel.config.updates.unable_to_fetch"
)}
alert-type="error"
>
${this._error}
</ha-alert>`
: ""}
${this._supervisorUpdates?.map(
<div class="title">
${this.hass.localize("ui.panel.config.updates.title", {
count: this.supervisorUpdates.length,
})}
</div>
${updates.map(
(update) => html`
<ha-alert
.title=${update.update_type === "addon"
? update.name
: SUPERVISOR_UPDATE_NAMES[update.update_type!]}
>
<span slot="icon" class="icon">
<paper-icon-item>
<span slot="item-icon" class="icon">
${update.update_type === "addon"
? update.icon
? html`<img src="/api/hassio${update.icon}" />`
: html`<ha-svg-icon .path=${mdiPackageVariant}></ha-svg-icon>`
: html`<ha-logo-svg></ha-logo-svg>`}
</span>
${this.hass.localize("ui.panel.config.updates.version_available", {
version_available: update.version_latest,
})}
<a href="/hassio${update.panel_path}" slot="action">
<paper-item-body two-line>
${update.update_type === "addon"
? update.name
: SUPERVISOR_UPDATE_NAMES[update.update_type!]}
<div secondary>
${this.hass.localize(
"ui.panel.config.updates.version_available",
{
version_available: update.version_latest,
}
)}
</div>
</paper-item-body>
<a href="/hassio${update.panel_path}">
<mwc-button
.label=${this.hass.localize("ui.panel.config.updates.review")}
.label=${this.hass.localize("ui.panel.config.updates.show")}
>
</mwc-button>
</a>
</ha-alert>
</paper-icon-item>
`
)}
${!this._showAll && !this.narrow ? html`<div class="divider"></div>` : ""}
${!this._showAll && this.supervisorUpdates.length >= 4
? html`
<button class="link show-all" @click=${this._showAllClicked}>
${this.hass.localize("ui.panel.config.updates.show_all_updates")}
</button>
`
: ""}
`;
}
private async _loadSupervisorUpdates(): Promise<void> {
try {
this._supervisorUpdates = await fetchSupervisorAvailableUpdates(
this.hass
);
} catch (err) {
this._error = extractApiErrorMessage(err);
}
private _showAllClicked() {
this._showAll = true;
}
static get styles(): CSSResultGroup {
return css`
a {
text-decoration: none;
color: var(--primary-text-color);
}
.icon {
place-self: center;
}
img,
ha-svg-icon,
ha-logo-svg {
width: var(--mdc-icon-size, 32px);
height: var(--mdc-icon-size, 32px);
padding-right: 12px;
display: block;
color: var(--secondary-text-color);
}
`;
static get styles(): CSSResultGroup[] {
return [
buttonLinkStyle,
css`
.title {
font-size: 16px;
padding: 16px;
padding-bottom: 0;
}
a {
text-decoration: none;
color: var(--primary-text-color);
}
.icon {
display: inline-flex;
height: 100%;
align-items: center;
}
img,
ha-svg-icon,
ha-logo-svg {
--mdc-icon-size: 32px;
max-height: 32px;
width: 32px;
}
ha-logo-svg {
color: var(--secondary-text-color);
}
button.show-all {
color: var(--primary-color);
text-decoration: none;
margin: 8px 16px;
}
.divider::before {
content: " ";
display: block;
height: 1px;
background-color: var(--divider-color);
}
`,
];
}
}
@@ -1,6 +1,8 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card";
import "../../../../components/ha-chip";
import "../../../../components/ha-chip-set";
import { showAutomationEditor } from "../../../../data/automation";
import {
@@ -10,6 +12,12 @@ import {
import { showScriptEditor } from "../../../../data/script";
import { HomeAssistant } from "../../../../types";
declare global {
interface HASSDomEvents {
"entry-selected": undefined;
}
}
export abstract class HaDeviceAutomationCard<
T extends DeviceAutomation
> extends LitElement {
@@ -55,29 +63,34 @@ export abstract class HaDeviceAutomationCard<
return html`
<h3>${this.hass.localize(this.headerKey)}</h3>
<div class="content">
<ha-chip-set
@chip-clicked=${this._handleAutomationClicked}
.items=${this.automations.map((automation) =>
this._localizeDeviceAutomation(this.hass, automation)
<ha-chip-set>
${this.automations.map(
(automation, idx) =>
html`
<ha-chip .index=${idx} @click=${this._handleAutomationClicked}>
${this._localizeDeviceAutomation(this.hass, automation)}
</ha-chip>
`
)}
>
</ha-chip-set>
</div>
`;
}
private _handleAutomationClicked(ev: CustomEvent) {
const automation = this.automations[ev.detail.index];
const automation = this.automations[(ev.currentTarget as any).index];
if (!automation) {
return;
}
if (this.script) {
showScriptEditor({ sequence: [automation as DeviceAction] });
fireEvent(this, "entry-selected");
return;
}
const data = {};
data[this.type] = [automation];
showAutomationEditor(data);
fireEvent(this, "entry-selected");
}
static get styles(): CSSResultGroup {
@@ -91,7 +91,7 @@ export class DialogDeviceAutomation extends LitElement {
}.create`
)}
>
<div @chip-clicked=${this.closeDialog}>
<div @entry-selected=${this.closeDialog}>
${this._triggers.length ||
this._conditions.length ||
this._actions.length
@@ -82,9 +82,9 @@ export class HaDeviceCard extends LitElement {
const device = devices.find((dev) => dev.id === deviceId);
return device
? computeDeviceName(device, this.hass)
: `(${this.hass.localize(
"ui.panel.config.integrations.config_entry.device_unavailable"
)})`;
: `<${this.hass.localize(
"ui.panel.config.integrations.config_entry.unknown_via_device"
)}>`;
}
static get styles(): CSSResultGroup {
@@ -218,7 +218,7 @@ export class HaConfigDevicePage extends LitElement {
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.tabs=${configSections.integrations}
.tabs=${configSections.devices}
.route=${this.route}
>
${
@@ -375,7 +375,7 @@ export class HaConfigDeviceDashboard extends LitElement {
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
.tabs=${configSections.integrations}
.tabs=${configSections.devices}
.route=${this.route}
.activeFilters=${activeFilters}
.numHidden=${this._numHiddenDevices}
+5 -6
View File
@@ -1,3 +1,4 @@
import "../../../layouts/hass-error-screen";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
@@ -9,11 +10,10 @@ import {
getEnergyPreferences,
} from "../../../data/energy";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
import "../../../components/ha-alert";
import { configSections } from "../ha-panel-config";
import "./components/ha-energy-device-settings";
import "./components/ha-energy-grid-settings";
import "./components/ha-energy-solar-settings";
@@ -68,14 +68,13 @@ class HaConfigEnergy extends LitElement {
}
return html`
<hass-tabs-subpage
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
.route=${this.route}
.tabs=${configSections.experiences}
.header=${this.hass.localize("ui.panel.config.energy.caption")}
>
<ha-alert>
${this.hass.localize("ui.panel.config.energy.new_device_info")}
@@ -113,7 +112,7 @@ class HaConfigEnergy extends LitElement {
@value-changed=${this._prefsChanged}
></ha-energy-device-settings>
</div>
</hass-tabs-subpage>
</hass-subpage>
`;
}
@@ -478,7 +478,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
? undefined
: "/config"}
.route=${this.route}
.tabs=${configSections.integrations}
.tabs=${configSections.devices}
.columns=${this._columns(
this.narrow,
this.hass.language,
+12
View File
@@ -8,11 +8,15 @@ export class HaConfigSection extends LitElement {
@property({ type: Boolean }) public vertical = false;
@property({ type: Boolean, attribute: "full-width" })
public fullWidth = false;
protected render() {
return html`
<div
class="content ${classMap({
narrow: !this.isWide,
"full-width": this.fullWidth,
})}"
>
<div class="header"><slot name="header"></slot></div>
@@ -111,6 +115,14 @@ export class HaConfigSection extends LitElement {
margin-right: 0;
max-width: 500px;
}
.full-width {
padding: 0;
}
.full-width .layout {
flex-direction: column;
}
`;
}
}
+123 -3
View File
@@ -1,6 +1,7 @@
import {
mdiAccount,
mdiBadgeAccountHorizontal,
mdiCog,
mdiDevices,
mdiHomeAssistant,
mdiInformation,
@@ -27,6 +28,10 @@ import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { listenMediaQuery } from "../../common/dom/media_query";
import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
import {
fetchSupervisorAvailableUpdates,
SupervisorAvailableUpdates,
} from "../../data/supervisor/supervisor";
import "../../layouts/hass-loading-screen";
import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
@@ -40,12 +45,88 @@ declare global {
}
export const configSections: { [name: string]: PageNavigation[] } = {
integrations: [
dashboard: [
{
path: "/config/integrations",
name: "Devices & Services",
description: "Integrations, devices, entities and areas",
iconPath: mdiDevices,
iconColor: "#0D47A1",
core: true,
},
{
path: "/config/automation",
name: "Automations",
description: "Automations, blueprints, scenes and scripts",
iconPath: mdiRobot,
iconColor: "#518C43",
components: ["automation", "blueprint", "scene", "script"],
},
{
path: "/config/helpers",
name: "Helpers",
description: "Elements that help build automations",
iconPath: mdiTools,
iconColor: "#4D2EA4",
core: true,
},
{
path: "/hassio",
name: "Add-ons & Backups",
description: "Create backups, check logs or reboot your system",
iconPath: mdiHomeAssistant,
iconColor: "#4084CD",
component: "hassio",
},
{
path: "/config/lovelace/dashboards",
name: "Dashboards",
description: "Create customized sets of cards to control your home",
iconPath: mdiViewDashboard,
iconColor: "#B1345C",
component: "lovelace",
},
{
path: "/config/energy",
name: "Energy",
description: "Monitor your energy production and consumption",
iconPath: mdiLightningBolt,
iconColor: "#F1C447",
component: "energy",
},
{
path: "/config/tags",
name: "Tags",
description:
"Trigger automations when a NFC tag, QR code, etc. is scanned",
iconPath: mdiNfcVariant,
iconColor: "#616161",
component: "tag",
},
{
path: "/config/person",
name: "People & Zones",
description: "Manage the people and zones that Home Assistant tracks",
iconPath: mdiAccount,
iconColor: "#E48629",
components: ["person", "zone", "users"],
},
{
path: "/config/core",
name: "Settings",
description: "Basic settings, server controls, logs and info",
iconPath: mdiCog,
iconColor: "#4A5963",
core: true,
},
],
devices: [
{
component: "integrations",
path: "/config/integrations",
translationKey: "ui.panel.config.integrations.caption",
iconPath: mdiPuzzle,
iconColor: "#2D338F",
core: true,
},
{
@@ -53,6 +134,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
path: "/config/devices",
translationKey: "ui.panel.config.devices.caption",
iconPath: mdiDevices,
iconColor: "#2D338F",
core: true,
},
{
@@ -60,6 +142,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
path: "/config/entities",
translationKey: "ui.panel.config.entities.caption",
iconPath: mdiShape,
iconColor: "#2D338F",
core: true,
},
{
@@ -67,33 +150,38 @@ export const configSections: { [name: string]: PageNavigation[] } = {
path: "/config/areas",
translationKey: "ui.panel.config.areas.caption",
iconPath: mdiSofa,
iconColor: "#2D338F",
core: true,
},
],
automation: [
automations: [
{
component: "blueprint",
path: "/config/blueprint",
translationKey: "ui.panel.config.blueprint.caption",
iconPath: mdiPaletteSwatch,
iconColor: "#518C43",
},
{
component: "automation",
path: "/config/automation",
translationKey: "ui.panel.config.automation.caption",
iconPath: mdiRobot,
iconColor: "#518C43",
},
{
component: "scene",
path: "/config/scene",
translationKey: "ui.panel.config.scene.caption",
iconPath: mdiPalette,
iconColor: "#518C43",
},
{
component: "script",
path: "/config/script",
translationKey: "ui.panel.config.script.caption",
iconPath: mdiScriptText,
iconColor: "#518C43",
},
],
helpers: [
@@ -102,21 +190,26 @@ export const configSections: { [name: string]: PageNavigation[] } = {
path: "/config/helpers",
translationKey: "ui.panel.config.helpers.caption",
iconPath: mdiTools,
iconColor: "#4D2EA4",
core: true,
},
],
experiences: [
tags: [
{
component: "tag",
path: "/config/tags",
translationKey: "ui.panel.config.tag.caption",
iconPath: mdiNfcVariant,
iconColor: "#616161",
},
],
energy: [
{
component: "energy",
path: "/config/energy",
translationKey: "ui.panel.config.energy.caption",
iconPath: mdiLightningBolt,
iconColor: "#F1C447",
},
],
lovelace: [
@@ -125,6 +218,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
path: "/config/lovelace/dashboards",
translationKey: "ui.panel.config.lovelace.caption",
iconPath: mdiViewDashboard,
iconColor: "#B1345C",
},
],
persons: [
@@ -133,18 +227,21 @@ export const configSections: { [name: string]: PageNavigation[] } = {
path: "/config/person",
translationKey: "ui.panel.config.person.caption",
iconPath: mdiAccount,
iconColor: "#E48629",
},
{
component: "zone",
path: "/config/zone",
translationKey: "ui.panel.config.zone.caption",
iconPath: mdiMapMarkerRadius,
iconColor: "#E48629",
},
{
component: "users",
path: "/config/users",
translationKey: "ui.panel.config.users.caption",
iconPath: mdiBadgeAccountHorizontal,
iconColor: "#E48629",
core: true,
advancedOnly: true,
},
@@ -155,6 +252,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
path: "/config/core",
translationKey: "ui.panel.config.core.caption",
iconPath: mdiHomeAssistant,
iconColor: "#4A5963",
core: true,
},
{
@@ -162,6 +260,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
path: "/config/server_control",
translationKey: "ui.panel.config.server_control.caption",
iconPath: mdiServer,
iconColor: "#4A5963",
core: true,
},
{
@@ -169,6 +268,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
path: "/config/logs",
translationKey: "ui.panel.config.logs.caption",
iconPath: mdiMathLog,
iconColor: "#4A5963",
core: true,
},
{
@@ -176,6 +276,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
path: "/config/info",
translationKey: "ui.panel.config.info.caption",
iconPath: mdiInformation,
iconColor: "#4A5963",
core: true,
},
],
@@ -316,6 +417,8 @@ class HaPanelConfig extends HassRouterPage {
@state() private _cloudStatus?: CloudStatus;
@state() private _supervisorUpdates?: SupervisorAvailableUpdates[] | null;
private _listeners: Array<() => void> = [];
public connectedCallback() {
@@ -345,6 +448,11 @@ class HaPanelConfig extends HassRouterPage {
if (isComponentLoaded(this.hass, "cloud")) {
this._updateCloudStatus();
}
if (isComponentLoaded(this.hass, "hassio")) {
this._loadSupervisorUpdates();
} else {
this._supervisorUpdates = null;
}
this.addEventListener("ha-refresh-cloud-status", () =>
this._updateCloudStatus()
);
@@ -375,6 +483,7 @@ class HaPanelConfig extends HassRouterPage {
isWide,
narrow: this.narrow,
cloudStatus: this._cloudStatus,
supervisorUpdates: this._supervisorUpdates,
});
} else {
el.route = this.routeTail;
@@ -383,6 +492,7 @@ class HaPanelConfig extends HassRouterPage {
el.isWide = isWide;
el.narrow = this.narrow;
el.cloudStatus = this._cloudStatus;
el.supervisorUpdates = this._supervisorUpdates;
}
}
@@ -400,6 +510,16 @@ class HaPanelConfig extends HassRouterPage {
setTimeout(() => this._updateCloudStatus(), 5000);
}
}
private async _loadSupervisorUpdates(): Promise<void> {
try {
this._supervisorUpdates = await fetchSupervisorAvailableUpdates(
this.hass
);
} catch (err) {
this._supervisorUpdates = null;
}
}
}
declare global {
@@ -319,7 +319,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.integrations}
.tabs=${configSections.devices}
>
${this.narrow
? html`
@@ -147,7 +147,7 @@ class HaSceneDashboard extends LitElement {
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.automation}
.tabs=${configSections.automations}
.columns=${this._columns(this.hass.language)}
id="entity_id"
.data=${this._scenes(this.scenes, this._filteredScenes)}
+1 -1
View File
@@ -202,7 +202,7 @@ export class HaSceneEditor extends SubscribeMixin(
.narrow=${this.narrow}
.route=${this.route}
.backCallback=${this._backTapped}
.tabs=${configSections.automation}
.tabs=${configSections.automations}
>
<ha-button-menu
corner="BOTTOM_START"
+1 -1
View File
@@ -90,7 +90,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
.narrow=${this.narrow}
.route=${this.route}
.backCallback=${this._backTapped}
.tabs=${configSections.automation}
.tabs=${configSections.automations}
>
<ha-button-menu
corner="BOTTOM_START"
+1 -1
View File
@@ -174,7 +174,7 @@ class HaScriptPicker extends LitElement {
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.automation}
.tabs=${configSections.automations}
.columns=${this._columns(this.narrow, this.hass.locale)}
.data=${this._scripts(this.scripts, this._filteredScripts)}
.activeFilters=${this._activeFilters}
+1 -1
View File
@@ -108,7 +108,7 @@ export class HaScriptTrace extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${configSections.automation}
.tabs=${configSections.automations}
>
${this.narrow
? html`<span slot="header"> ${title} </span>
+1 -1
View File
@@ -180,7 +180,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.experiences}
.tabs=${configSections.tags}
.columns=${this._columns(
this.narrow,
this._canWriteTags,
+5
View File
@@ -139,6 +139,11 @@ class HaPanelHistory extends LitElement {
};
this._entityId = extractSearchParam("entity_id") ?? "";
const startDate = extractSearchParam("start_date");
if (startDate) {
this._startDate = new Date(startDate);
}
}
protected updated(changedProps: PropertyValues) {
+5
View File
@@ -161,6 +161,11 @@ export class HaPanelLogbook extends LitElement {
};
this._entityId = extractSearchParam("entity_id") ?? "";
const startDate = extractSearchParam("start_date");
if (startDate) {
this._startDate = new Date(startDate);
}
}
protected updated(changedProps: PropertyValues<this>) {
+8 -5
View File
@@ -9,7 +9,6 @@ import {
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { relativeTime } from "../../../common/datetime/relative_time";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
@@ -17,6 +16,7 @@ import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/entity/state-badge";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import "../../../components/ha-relative-time";
import { UNAVAILABLE_STATES } from "../../../data/entity";
import {
ActionHandlerEvent,
@@ -325,10 +325,13 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
></hui-timestamp-display>
`
: entityConf.show_last_changed
? relativeTime(
new Date(stateObj.last_changed),
this.hass!.locale
)
? html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_changed}
capitalize
></ha-relative-time>
`
: computeStateDisplay(
this.hass!.localize,
stateObj,
@@ -25,6 +25,7 @@ import {
ThermostatCardConfig,
} from "../cards/types";
import { LovelaceRowConfig } from "../entity-rows/types";
import { ButtonsHeaderFooterConfig } from "../header-footer/types";
const HIDE_DOMAIN = new Set([
"automation",
@@ -97,6 +98,8 @@ export const computeCards = (
? `${entityCardOptions.title} `.toLowerCase()
: undefined;
const footerEntities: ButtonsHeaderFooterConfig["entities"] = [];
for (const [entityId, stateObj] of states) {
const domain = computeDomain(entityId);
@@ -143,6 +146,12 @@ export const computeCards = (
show_forecast: false,
};
cards.push(cardConfig);
} else if (domain === "scene" || domain === "script") {
footerEntities.push({
entity: entityId,
show_icon: true,
show_name: true,
});
} else if (
domain === "sensor" &&
stateObj?.attributes.device_class === SENSOR_DEVICE_CLASS_BATTERY
@@ -168,15 +177,33 @@ export const computeCards = (
}
}
if (entities.length > 0) {
cards.unshift({
if (entities.length > 0 || footerEntities.length > 0) {
const card: EntitiesCardConfig = {
type: "entities",
entities,
...entityCardOptions,
});
};
if (footerEntities.length > 0) {
card.footer = {
type: "buttons",
entities: footerEntities,
} as ButtonsHeaderFooterConfig;
}
cards.unshift(card);
}
return cards;
if (cards.length < 2) {
return cards;
}
return [
{
type: "grid",
square: false,
columns: 1,
cards,
},
];
};
const computeDefaultViewStates = (
@@ -5,11 +5,10 @@ import "../cards/hui-entities-card";
import "../cards/hui-entity-button-card";
import "../cards/hui-entity-card";
import "../cards/hui-glance-card";
import "../cards/hui-horizontal-stack-card";
import "../cards/hui-grid-card";
import "../cards/hui-light-card";
import "../cards/hui-sensor-card";
import "../cards/hui-thermostat-card";
import "../cards/hui-vertical-stack-card";
import "../cards/hui-weather-forecast-card";
import {
createLovelaceElement,
@@ -23,59 +22,59 @@ const ALWAYS_LOADED_TYPES = new Set([
"button",
"entity-button",
"glance",
"horizontal-stack",
"grid",
"light",
"sensor",
"thermostat",
"vertical-stack",
"weather-forecast",
]);
const LAZY_LOAD_TYPES = {
"alarm-panel": () => import("../cards/hui-alarm-panel-card"),
area: () => import("../cards/hui-area-card"),
error: () => import("../cards/hui-error-card"),
calendar: () => import("../cards/hui-calendar-card"),
conditional: () => import("../cards/hui-conditional-card"),
"empty-state": () => import("../cards/hui-empty-state-card"),
"energy-usage-graph": () =>
import("../cards/energy/hui-energy-usage-graph-card"),
"energy-solar-graph": () =>
import("../cards/energy/hui-energy-solar-graph-card"),
"energy-gas-graph": () => import("../cards/energy/hui-energy-gas-graph-card"),
"energy-devices-graph": () =>
import("../cards/energy/hui-energy-devices-graph-card"),
"energy-sources-table": () =>
import("../cards/energy/hui-energy-sources-table-card"),
"energy-distribution": () =>
import("../cards/energy/hui-energy-distribution-card"),
"energy-solar-consumed-gauge": () =>
import("../cards/energy/hui-energy-solar-consumed-gauge-card"),
"energy-grid-neutrality-gauge": () =>
import("../cards/energy/hui-energy-grid-neutrality-gauge-card"),
"energy-carbon-consumed-gauge": () =>
import("../cards/energy/hui-energy-carbon-consumed-gauge-card"),
"energy-date-selection": () =>
import("../cards/energy/hui-energy-date-selection-card"),
grid: () => import("../cards/hui-grid-card"),
starting: () => import("../cards/hui-starting-card"),
"energy-devices-graph": () =>
import("../cards/energy/hui-energy-devices-graph-card"),
"energy-distribution": () =>
import("../cards/energy/hui-energy-distribution-card"),
"energy-gas-graph": () => import("../cards/energy/hui-energy-gas-graph-card"),
"energy-grid-neutrality-gauge": () =>
import("../cards/energy/hui-energy-grid-neutrality-gauge-card"),
"energy-solar-consumed-gauge": () =>
import("../cards/energy/hui-energy-solar-consumed-gauge-card"),
"energy-solar-graph": () =>
import("../cards/energy/hui-energy-solar-graph-card"),
"energy-sources-table": () =>
import("../cards/energy/hui-energy-sources-table-card"),
"energy-usage-graph": () =>
import("../cards/energy/hui-energy-usage-graph-card"),
"entity-filter": () => import("../cards/hui-entity-filter-card"),
error: () => import("../cards/hui-error-card"),
gauge: () => import("../cards/hui-gauge-card"),
"history-graph": () => import("../cards/hui-history-graph-card"),
"horizontal-stack": () => import("../cards/hui-horizontal-stack-card"),
humidifier: () => import("../cards/hui-humidifier-card"),
iframe: () => import("../cards/hui-iframe-card"),
logbook: () => import("../cards/hui-logbook-card"),
map: () => import("../cards/hui-map-card"),
markdown: () => import("../cards/hui-markdown-card"),
"media-control": () => import("../cards/hui-media-control-card"),
"picture-elements": () => import("../cards/hui-picture-elements-card"),
"picture-entity": () => import("../cards/hui-picture-entity-card"),
"picture-glance": () => import("../cards/hui-picture-glance-card"),
picture: () => import("../cards/hui-picture-card"),
"plant-status": () => import("../cards/hui-plant-status-card"),
"safe-mode": () => import("../cards/hui-safe-mode-card"),
"shopping-list": () => import("../cards/hui-shopping-list-card"),
conditional: () => import("../cards/hui-conditional-card"),
gauge: () => import("../cards/hui-gauge-card"),
"history-graph": () => import("../cards/hui-history-graph-card"),
starting: () => import("../cards/hui-starting-card"),
"statistics-graph": () => import("../cards/hui-statistics-graph-card"),
iframe: () => import("../cards/hui-iframe-card"),
map: () => import("../cards/hui-map-card"),
markdown: () => import("../cards/hui-markdown-card"),
picture: () => import("../cards/hui-picture-card"),
calendar: () => import("../cards/hui-calendar-card"),
logbook: () => import("../cards/hui-logbook-card"),
"vertical-stack": () => import("../cards/hui-vertical-stack-card"),
};
// This will not return an error card but will throw the error
+5 -2
View File
@@ -1,15 +1,17 @@
import { ActionConfig } from "../../../data/lovelace";
import { EntityConfig } from "../entity-rows/types";
import { EntitiesCardEntityConfig } from "../cards/types";
export interface LovelaceHeaderFooterConfig {
type: string;
}
export interface ButtonsHeaderFooterConfig extends LovelaceHeaderFooterConfig {
entities: Array<string | EntityConfig>;
type: "buttons";
entities: Array<string | EntitiesCardEntityConfig>;
}
export interface GraphHeaderFooterConfig extends LovelaceHeaderFooterConfig {
type: "graph";
entity: string;
detail?: number;
hours_to_show?: number;
@@ -20,6 +22,7 @@ export interface GraphHeaderFooterConfig extends LovelaceHeaderFooterConfig {
}
export interface PictureHeaderFooterConfig extends LovelaceHeaderFooterConfig {
type: "picture";
image: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
+7 -8
View File
@@ -915,7 +915,6 @@
},
"config": {
"header": "Configure Home Assistant",
"introduction": "In this view it is possible to configure your components and Home Assistant. Not everything is possible to configure from the UI yet, but we're working on it.",
"advanced_mode": {
"hint_enable": "Missing config options? Enable advanced mode on",
"link_profile_page": "your profile page"
@@ -927,9 +926,11 @@
"learn_more": "Learn more"
},
"updates": {
"title": "{count} {count, plural,\n one {update}\n other {updates}\n}",
"unable_to_fetch": "Unable to fetch available updates",
"version_available": "Version {version_available} is available",
"review": "review"
"show_all_updates": "Show all updates",
"show": "show"
},
"areas": {
"caption": "Areas",
@@ -2370,11 +2371,10 @@
"enable_restart_confirm": "Restart Home Assistant to finish enabling this integration",
"disable_error": "Enabling or disabling of the integration failed",
"manuf": "by {manufacturer}",
"hub": "Connected via",
"via": "Connected via",
"firmware": "Firmware: {version}",
"unnamed_entry": "Unnamed entry",
"device_unavailable": "Device unavailable",
"entity_unavailable": "Entity unavailable",
"unknown_via_device": "Unknown device",
"area": "In {area}",
"no_area": "No Area",
"not_loaded": "Not loaded",
@@ -4166,7 +4166,7 @@
"save": "[%key:ui::common::save%]",
"close": "[%key:ui::common::close%]",
"menu": "[%key:ui::common::menu%]",
"review": "[%key:ui::panel::config::updates::review%]",
"show": "[%key:ui::panel::config::updates::show%]",
"show_more": "Show more information about this",
"update_available": "{count, plural,\n one {Update}\n other {{count} updates}\n} pending",
"update": "Update",
@@ -4180,8 +4180,7 @@
"update_name": "Update {name}",
"open_release_notes": "Open release notes",
"create_backup": "Create backup before updating",
"description": "There is an update available for the {name}. You have {version} installed. Click update to update to version {newest_version}",
"core_note": "The supervisor will roll back to version {version} if your instance does not come up after the update.",
"description": "You have {version} installed. Click update to update to version {newest_version}",
"updating": "Updating {name} to version {version}",
"creating_backup": "Creating backup of {name}"
},