Merge branch 'rc'

This commit is contained in:
Bram Kragten 2025-01-06 18:10:30 +01:00
commit 926f5e3cd8
46 changed files with 192 additions and 180 deletions

View File

@ -7,7 +7,7 @@ body:
value: | value: |
Make sure you are running the [latest version of Home Assistant][releases] before reporting an issue. Make sure you are running the [latest version of Home Assistant][releases] before reporting an issue.
If you have a feature or enhancement request for the frontend, please [start an discussion][fr] instead of creating an issue. If you have a feature or enhancement request for the frontend, please [start a discussion][fr] instead of creating an issue.
**Please do not report issues for custom cards.** **Please do not report issues for custom cards.**

View File

@ -2,7 +2,7 @@ blank_issues_enabled: false
contact_links: contact_links:
- name: Request a feature for the UI / Dashboards - name: Request a feature for the UI / Dashboards
url: https://github.com/home-assistant/frontend/discussions/category_choices url: https://github.com/home-assistant/frontend/discussions/category_choices
about: Request an new feature for the Home Assistant frontend. about: Request a new feature for the Home Assistant frontend.
- name: Report a bug that is NOT related to the UI / Dashboards - name: Report a bug that is NOT related to the UI / Dashboards
url: https://github.com/home-assistant/core/issues url: https://github.com/home-assistant/core/issues
about: This is the issue tracker for our frontend. Please report other issues in the backend ("core") repository. about: This is the issue tracker for our frontend. Please report other issues in the backend ("core") repository.

View File

@ -25,7 +25,7 @@ Home Assistant Cast is made up of two separate applications:
### Setting dev variables ### Setting dev variables
Open `src/cast/dev_const.ts` and change `CAST_DEV_APP_ID` to the ID of the app you just created. And set the `CAST_DEV_HASS_URL` to the url of you development machine. Open `src/cast/dev_const.ts` and change `CAST_DEV_APP_ID` to the ID of the app you just created. And set the `CAST_DEV_HASS_URL` to the url of your development machine.
### Changing configuration ### Changing configuration

View File

@ -252,7 +252,7 @@ export class HcConnect extends LitElement {
this.loading = false; this.loading = false;
return; return;
} finally { } finally {
// Clear url if we have a auth callback in url. // Clear url if we have an auth callback in url.
if (location.search.includes("auth_callback=1")) { if (location.search.includes("auth_callback=1")) {
history.replaceState(null, "", location.pathname); history.replaceState(null, "", location.pathname);
} }

View File

@ -42,7 +42,7 @@ In most cases, Create can be paired with Delete, and Add can be paired with Remo
## Add ## Add
An already-exisiting item. An already-existing item.
For example: For example:

View File

@ -19,7 +19,7 @@ The alert offers four severity levels that set a distinctive icon and color.
</ha-alert> </ha-alert>
<ha-alert alert-type="warning"> <ha-alert alert-type="warning">
This is an warning alert — check it out! This is a warning alert — check it out!
</ha-alert> </ha-alert>
<ha-alert alert-type="info"> <ha-alert alert-type="info">
@ -27,7 +27,7 @@ The alert offers four severity levels that set a distinctive icon and color.
</ha-alert> </ha-alert>
<ha-alert alert-type="success"> <ha-alert alert-type="success">
This is an success alert — check it out! This is a success alert — check it out!
</ha-alert> </ha-alert>
**Note:** This component is by <a href="https://mui.com/components/alert/" rel="noopener noreferrer" target="_blank">MUI</a> and is not documented in the <a href="https://material.io" rel="noopener noreferrer" target="_blank">Material Design guidelines</a>. **Note:** This component is by <a href="https://mui.com/components/alert/" rel="noopener noreferrer" target="_blank">MUI</a> and is not documented in the <a href="https://material.io" rel="noopener noreferrer" target="_blank">Material Design guidelines</a>.
@ -95,7 +95,7 @@ Actions must have a tab index of 0 so that they can be reached by keyboard-only
</ha-alert> </ha-alert>
<ha-alert alert-type="warning"> <ha-alert alert-type="warning">
This is an warning alert — check it out! This is a warning alert — check it out!
</ha-alert> </ha-alert>
<ha-alert alert-type="info"> <ha-alert alert-type="info">
@ -103,7 +103,7 @@ Actions must have a tab index of 0 so that they can be reached by keyboard-only
</ha-alert> </ha-alert>
<ha-alert alert-type="success"> <ha-alert alert-type="success">
This is an success alert — check it out! This is a success alert — check it out!
</ha-alert> </ha-alert>
```html ```html
@ -122,37 +122,37 @@ Actions must have a tab index of 0 so that they can be reached by keyboard-only
The `title ` option should not be used without a description. The `title ` option should not be used without a description.
<ha-alert alert-type="success" title="Success"> <ha-alert alert-type="success" title="Success">
This is an success alert — check it out! This is a success alert — check it out!
</ha-alert> </ha-alert>
```html ```html
<ha-alert alert-type="success" title="Success"> <ha-alert alert-type="success" title="Success">
This is an success alert — check it out! This is a success alert — check it out!
</ha-alert> </ha-alert>
``` ```
**Dismissable** **Dismissable**
<ha-alert alert-type="success" dismissable> <ha-alert alert-type="success" dismissable>
This is an success alert — check it out! This is a success alert — check it out!
</ha-alert> </ha-alert>
```html ```html
<ha-alert alert-type="success" dismissable> <ha-alert alert-type="success" dismissable>
This is an success alert — check it out! This is a success alert — check it out!
</ha-alert> </ha-alert>
``` ```
**Slotted action** **Slotted action**
<ha-alert alert-type="success"> <ha-alert alert-type="success">
This is an success alert — check it out! This is a success alert — check it out!
<mwc-button slot="action" label="Undo"></mwc-button> <mwc-button slot="action" label="Undo"></mwc-button>
</ha-alert> </ha-alert>
```html ```html
<ha-alert alert-type="success"> <ha-alert alert-type="success">
This is an success alert — check it out! This is a success alert — check it out!
<mwc-button slot="action" label="Undo"></mwc-button> <mwc-button slot="action" label="Undo"></mwc-button>
</ha-alert> </ha-alert>
``` ```

View File

@ -20,7 +20,7 @@ export class DemoHaFaded extends LitElement {
<ha-faded><span>${LONG_TEXT}</span></ha-faded> <ha-faded><span>${LONG_TEXT}</span></ha-faded>
<h3>No text</h3> <h3>No text</h3>
<ha-faded><span></span></ha-faded> <ha-faded><span></span></ha-faded>
<h3>Smal text</h3> <h3>Small text</h3>
<ha-faded><span>${SMALL_TEXT}</span></ha-faded> <ha-faded><span>${SMALL_TEXT}</span></ha-faded>
<h3>Long text in markdown</h3> <h3>Long text in markdown</h3>
<ha-faded> <ha-faded>

View File

@ -76,7 +76,7 @@ export class DemoHaHsColorPicker extends LitElement {
@change=${this._saturationChanged} @change=${this._saturationChanged}
> >
</ha-slider> </ha-slider>
<p>Color Brighness : ${this.brightness}</p> <p>Color Brightness : ${this.brightness}</p>
<ha-slider <ha-slider
labeled labeled
step="1" step="1"

View File

@ -18,10 +18,7 @@ import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-faded"; import "../../../src/components/ha-faded";
import "../../../src/components/ha-icon-button"; import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-markdown"; import "../../../src/components/ha-markdown";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
import "../../../src/components/ha-switch";
import type { HaSwitch } from "../../../src/components/ha-switch";
import type { HassioAddonDetails } from "../../../src/data/hassio/addon"; import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
import { import {
fetchHassioAddonChangelog, fetchHassioAddonChangelog,
@ -163,19 +160,6 @@ class UpdateAvailableCard extends LitElement {
)} )}
</p> </p>
</div> </div>
${["core", "addon"].includes(this._updateType)
? html`
<hr />
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize(
"update_available.create_backup"
)}
</span>
<ha-switch id="create_backup" checked></ha-switch>
</ha-settings-row>
`
: nothing}
` `
: html`<ha-circular-progress : html`<ha-circular-progress
aria-label="Updating" aria-label="Updating"
@ -243,19 +227,6 @@ class UpdateAvailableCard extends LitElement {
} }
} }
get _shouldCreateBackup(): boolean {
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
return false;
}
const createBackupSwitch = this.shadowRoot?.getElementById(
"create-backup"
) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
}
return true;
}
get _version(): string { get _version(): string {
return this._updateType return this._updateType
? this._updateType === "addon" ? this._updateType === "addon"
@ -370,23 +341,14 @@ class UpdateAvailableCard extends LitElement {
} }
private async _update() { private async _update() {
if (this._shouldCreateBackup && this.supervisor.info.state === "freeze") {
this._error = this.supervisor.localize("backup.backup_already_running");
return;
}
this._error = undefined; this._error = undefined;
this._updating = true; this._updating = true;
try { try {
if (this._updateType === "addon") { if (this._updateType === "addon") {
await updateHassioAddon( await updateHassioAddon(this.hass, this.addonSlug!);
this.hass,
this.addonSlug!,
this._shouldCreateBackup
);
} else if (this._updateType === "core") { } else if (this._updateType === "core") {
await updateCore(this.hass, this._shouldCreateBackup); await updateCore(this.hass);
} else if (this._updateType === "os") { } else if (this._updateType === "os") {
await updateOS(this.hass); await updateOS(this.hass);
} else if (this._updateType === "supervisor") { } else if (this._updateType === "supervisor") {
@ -436,11 +398,6 @@ class UpdateAvailableCard extends LitElement {
padding-bottom: 8px; padding-bottom: 8px;
} }
ha-settings-row {
padding: 0;
margin-bottom: -16px;
}
hr { hr {
border-color: var(--divider-color); border-color: var(--divider-color);
border-bottom: none; border-bottom: none;

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20250103.0" version = "20250106.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@ -30,7 +30,7 @@ set -e
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
# parse input paramters # parse input parameters
if [ -n "$DEVCONTAINER" ]; then if [ -n "$DEVCONTAINER" ]; then
frontendPort=8123 frontendPort=8123
else else

View File

@ -19,9 +19,9 @@ type ReactiveElementClassWithTransformers = typeof ReactiveElement & {
}; };
/** /**
* Specifies an tranformer callback that is run when the value of the decorated property, or any of the properties in the watching array, changes. * Specifies a transformer callback that is run when the value of the decorated property, or any of the properties in the watching array, changes.
* The result of the tranformer is assigned to the decorated property. * The result of the transformer is assigned to the decorated property.
* The tranformer receives the current as arguments. * The transformer receives the current as argument.
*/ */
export const transform = export const transform =
<T, V>(config: { <T, V>(config: {

View File

@ -65,7 +65,7 @@ export const computeAttributeValueDisplay = (
return formattedValue; return formattedValue;
} }
// Special handling in case this is a string with an known format // Special handling in case this is a string with a known format
if (typeof attributeValue === "string") { if (typeof attributeValue === "string") {
// Date handling // Date handling
if (isDate(attributeValue, true)) { if (isDate(attributeValue, true)) {

View File

@ -166,7 +166,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
"channel", "channel",
"channels", "channels",
"composer", "composer",
"contibuting_artist", "contributing_artist",
"episode", "episode",
"game", "game",
"genre", "genre",

View File

@ -1,4 +1,4 @@
/** Return an color representing a state. */ /** Return a color representing a state. */
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE } from "../../data/entity"; import { UNAVAILABLE } from "../../data/entity";
import type { GroupEntity } from "../../data/group"; import type { GroupEntity } from "../../data/group";

View File

@ -406,7 +406,7 @@ function _doScore(
// this would be the beginning of a new match (i.e. there would be a gap before this location) // this would be the beginning of a new match (i.e. there would be a gap before this location)
score += isGapLocation ? 2 : 0; score += isGapLocation ? 2 : 0;
} else { } 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 // this is part of a contiguous match, so give it a slight bonus, but do so only if it would not be a preferred gap location
score += isGapLocation ? 0 : 1; score += isGapLocation ? 0 : 1;
} }

View File

@ -49,8 +49,8 @@ const NODE_WIDTH = 15;
const FONT_SIZE = 12; const FONT_SIZE = 12;
const MIN_DISTANCE = FONT_SIZE / 2; const MIN_DISTANCE = FONT_SIZE / 2;
@customElement("sankey-chart") @customElement("ha-sankey-chart")
export class SankeyChart extends LitElement { export class HaSankeyChart extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public data: SankeyChartData = { @property({ attribute: false }) public data: SankeyChartData = {
@ -315,7 +315,7 @@ export class SankeyChart extends LitElement {
} else { } else {
totalSize = section.nodes.reduce((sum, b) => sum + b.size, 0); totalSize = section.nodes.reduce((sum, b) => sum + b.size, 0);
} }
// calc margin betwee boxes // calc margin between boxes
const emptySpace = sectionSize - totalSize; const emptySpace = sectionSize - totalSize;
const spacerSize = emptySpace / (section.nodes.length - 1); const spacerSize = emptySpace / (section.nodes.length - 1);
@ -539,6 +539,6 @@ export class SankeyChart extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"sankey-chart": SankeyChart; "ha-sankey-chart": HaSankeyChart;
} }
} }

View File

@ -38,12 +38,11 @@ class StateInfo extends LitElement {
${this.inDialog ${this.inDialog
? html`<div class="time-ago"> ? html`<div class="time-ago">
<ha-relative-time <ha-relative-time
id="last_changed"
.hass=${this.hass} .hass=${this.hass}
.datetime=${this.stateObj.last_changed} .datetime=${this.stateObj.last_changed}
capitalize capitalize
></ha-relative-time> ></ha-relative-time>
<simple-tooltip animation-delay="0" for="last_changed"> <simple-tooltip animation-delay="0">
<div> <div>
<div class="row"> <div class="row">
<span class="column-name"> <span class="column-name">
@ -99,6 +98,7 @@ class StateInfo extends LitElement {
height: 100%; height: 100%;
min-width: 0; min-width: 0;
text-align: var(--float-start); text-align: var(--float-start);
position: relative;
} }
.name { .name {

View File

@ -288,7 +288,7 @@ class HaHLSPlayer extends LitElement {
hls.on(Hls.Events.ERROR, (_event, data: any) => { hls.on(Hls.Events.ERROR, (_event, data: any) => {
// Some errors are recovered automatically by the hls player itself, and the others handled // Some errors are recovered automatically by the hls player itself, and the others handled
// in this function require special actions to recover. Errors retried in this function // in this function require special actions to recover. Errors retried in this function
// are done with backoff to not cause unecessary failures. // are done with backoff to not cause unnecessary failures.
if (!data.fatal) { if (!data.fatal) {
return; return;
} }

View File

@ -56,7 +56,7 @@ export class HaIcon extends LitElement {
return nothing; return nothing;
} }
if (this._legacy) { if (this._legacy) {
return html`<!-- @ts-ignore we don't provice the iron-icon element --> return html`<!-- @ts-ignore we don't provide the iron-icon element -->
<iron-icon .icon=${this.icon}></iron-icon>`; <iron-icon .icon=${this.icon}></iron-icon>`;
} }
return html`<ha-svg-icon return html`<ha-svg-icon

View File

@ -7,7 +7,7 @@ import {
import { css } from "lit"; import { css } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
// workaround to be able to overlay an dialog with another dialog // workaround to be able to overlay a dialog with another dialog
MdDialog.addInitializer(async (instance) => { MdDialog.addInitializer(async (instance) => {
await instance.updateComplete; await instance.updateComplete;
@ -197,7 +197,7 @@ export class HaMdDialog extends MdDialog {
} }
// by default the dialog open/close animation will be from/to the top // by default the dialog open/close animation will be from/to the top
// but if we have a special mobile dialog which is at the bottom of the screen, an from bottom animation can be used: // but if we have a special mobile dialog which is at the bottom of the screen, a from bottom animation can be used:
const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = { const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = {
...DIALOG_DEFAULT_OPEN_ANIMATION, ...DIALOG_DEFAULT_OPEN_ANIMATION,
dialog: [ dialog: [

View File

@ -390,7 +390,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this.hass.locale this.hass.locale
); );
// Show the supervisor as beeing part of configuration // Show the supervisor as being part of configuration
const selectedPanel = this.route.path?.startsWith("/hassio/") const selectedPanel = this.route.path?.startsWith("/hassio/")
? "config" ? "config"
: this.hass.panelUrl; : this.hass.panelUrl;
@ -632,7 +632,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return html`<a return html`<a
class=${classMap({ class=${classMap({
profile: true, profile: true,
// Mimick behavior that paper-listbox provides // Mimic behavior that paper-listbox provides
"iron-selected": this.hass.panelUrl === "profile", "iron-selected": this.hass.panelUrl === "profile",
})} })}
href="/profile" href="/profile"

View File

@ -444,7 +444,7 @@ const getEnergyData = async (
hass.config hass.config
) as boolean) ) as boolean)
) { ) {
// When comparing a month (or multiple), we want to start at the begining of the month // When comparing a month (or multiple), we want to start at the beginning of the month
startCompare = calcDate( startCompare = calcDate(
start, start,
addMonths, addMonths,

View File

@ -313,8 +313,7 @@ export const installHassioAddon = async (
export const updateHassioAddon = async ( export const updateHassioAddon = async (
hass: HomeAssistant, hass: HomeAssistant,
slug: string, slug: string
backup: boolean
): Promise<void> => { ): Promise<void> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) { if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({ await hass.callWS({
@ -322,13 +321,11 @@ export const updateHassioAddon = async (
endpoint: `/store/addons/${slug}/update`, endpoint: `/store/addons/${slug}/update`,
method: "post", method: "post",
timeout: null, timeout: null,
data: { backup },
}); });
} else { } else {
await hass.callApi<HassioResponse<void>>( await hass.callApi<HassioResponse<void>>(
"POST", "POST",
`hassio/addons/${slug}/update`, `hassio/addons/${slug}/update`
{ backup }
); );
} }
}; };

View File

@ -19,7 +19,7 @@ export interface LogbookStreamMessage {
events: LogbookEntry[]; events: LogbookEntry[];
start_time?: number; // Start time of this historical chunk start_time?: number; // Start time of this historical chunk
end_time?: number; // End time of this historical chunk end_time?: number; // End time of this historical chunk
partial?: boolean; // Indiciates more historical chunks are coming partial?: boolean; // Indicates more historical chunks are coming
} }
export interface LogbookEntry { export interface LogbookEntry {

View File

@ -6,18 +6,15 @@ export const restartCore = async (hass: HomeAssistant) => {
await hass.callService("homeassistant", "restart"); await hass.callService("homeassistant", "restart");
}; };
export const updateCore = async (hass: HomeAssistant, backup: boolean) => { export const updateCore = async (hass: HomeAssistant) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) { if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({ await hass.callWS({
type: "supervisor/api", type: "supervisor/api",
endpoint: "/core/update", endpoint: "/core/update",
method: "post", method: "post",
timeout: null, timeout: null,
data: { backup },
}); });
} else { } else {
await hass.callApi<HassioResponse<void>>("POST", `hassio/core/update`, { await hass.callApi<HassioResponse<void>>("POST", "hassio/core/update");
backup,
});
} }
}; };

View File

@ -42,7 +42,7 @@ export const enum InclusionStrategy {
* *
* Issues a warning if Security S0 is not supported or the secure bootstrapping fails. * Issues a warning if Security S0 is not supported or the secure bootstrapping fails.
* *
* **Not recommended** because S0 should be used sparingly and S2 preferred whereever possible. * **Not recommended** because S0 should be used sparingly and S2 preferred wherever possible.
*/ */
Security_S0, Security_S0,
/** /**

View File

@ -43,7 +43,7 @@ const initRouting = () => {
// CORS must be forced to work for CSS images // CORS must be forced to work for CSS images
fetchOptions: { mode: "cors", credentials: "omit" }, fetchOptions: { mode: "cors", credentials: "omit" },
plugins: [ plugins: [
// Add 404 so we quicly respond to domains with missing images // Add 404 so we quickly respond to domains with missing images
new CacheableResponsePlugin({ statuses: [0, 200, 404] }), new CacheableResponsePlugin({ statuses: [0, 200, 404] }),
new ExpirationPlugin({ new ExpirationPlugin({
maxAgeSeconds: 60 * 60 * 24 * 30, maxAgeSeconds: 60 * 60 * 24 * 30,
@ -222,7 +222,7 @@ self.addEventListener("activate", () => {
// that didn't have a service worker loaded. // that didn't have a service worker loaded.
// Happens the first time they open the app without any // Happens the first time they open the app without any
// service worker registered. // service worker registered.
// This will serve code splitted bundles from SW. // This will serve code split bundles from SW.
clients.claim(); clients.claim();
}); });

View File

@ -644,7 +644,7 @@ export const demoServices: HassServices = {
example: "25", example: "25",
}, },
target_temp_high: { target_temp_high: {
description: "New target high tempereature for HVAC.", description: "New target high temperature for HVAC.",
example: "26", example: "26",
}, },
target_temp_low: { target_temp_low: {

View File

@ -267,7 +267,7 @@ class HaBackupConfigSchedule extends LitElement {
if (value !== RetentionPreset.CUSTOM) { if (value !== RetentionPreset.CUSTOM) {
const data = this._getData(this.value); const data = this._getData(this.value);
const retention = RETENTION_PRESETS[value]; const retention = RETENTION_PRESETS[value];
// Ensure we have at least 1 in defaut value because user can't select 0 // Ensure we have at least 1 in default value because user can't select 0
if (value !== RetentionPreset.FOREVER) { if (value !== RetentionPreset.FOREVER) {
retention.value = Math.max(retention.value, 1); retention.value = Math.max(retention.value, 1);
} }

View File

@ -31,14 +31,24 @@ class HaBackupOverviewBackups extends LitElement {
@property({ type: Boolean }) public fetching = false; @property({ type: Boolean }) public fetching = false;
private _lastSuccessfulBackup = memoizeOne((backups: BackupContent[]) => { private _sortedBackups = memoizeOne((backups: BackupContent[]) =>
const sortedBackups = backups backups
.filter((backup) => backup.with_automatic_settings) .filter((backup) => backup.with_automatic_settings)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
);
private _lastBackup = memoizeOne((backups: BackupContent[]) => {
const sortedBackups = this._sortedBackups(backups);
return sortedBackups[0] as BackupContent | undefined; return sortedBackups[0] as BackupContent | undefined;
}); });
private _lastUploadedBackup = memoizeOne((backups: BackupContent[]) => {
const sortedBackups = this._sortedBackups(backups);
return sortedBackups.find(
(backup) => backup.failed_agent_ids?.length === 0
);
});
private _nextBackupDescription(schedule: BackupScheduleState) { private _nextBackupDescription(schedule: BackupScheduleState) {
const time = getFormattedBackupTime(this.hass.locale, this.hass.config); const time = getFormattedBackupTime(this.hass.locale, this.hass.config);
@ -65,6 +75,8 @@ class HaBackupOverviewBackups extends LitElement {
} }
protected render() { protected render() {
const now = new Date();
if (this.fetching) { if (this.fetching) {
return html` return html`
<ha-backup-summary-card heading="Loading backups" status="loading"> <ha-backup-summary-card heading="Loading backups" status="loading">
@ -82,24 +94,28 @@ class HaBackupOverviewBackups extends LitElement {
`; `;
} }
const lastSuccessfulBackup = this._lastSuccessfulBackup(this.backups); const lastBackup = this._lastBackup(this.backups);
const lastAttempt = this.config.last_attempted_automatic_backup const nextBackupDescription = this._nextBackupDescription(
this.config.schedule.state
);
const lastAttemptDate = this.config.last_attempted_automatic_backup
? new Date(this.config.last_attempted_automatic_backup) ? new Date(this.config.last_attempted_automatic_backup)
: undefined; : new Date(0);
const lastCompletedBackupDate = this.config.last_completed_automatic_backup const lastCompletedDate = this.config.last_completed_automatic_backup
? new Date(this.config.last_completed_automatic_backup) ? new Date(this.config.last_completed_automatic_backup)
: undefined; : new Date(0);
const now = new Date(); // If last attempt is after last completed backup, show error
if (lastAttemptDate > lastCompletedDate) {
const description = `The last automatic backup triggered ${relativeTime(lastAttemptDate, this.hass.locale, now, true)} wasn't successful.`;
const lastUploadedBackup = this._lastUploadedBackup(this.backups);
const secondaryDescription = lastUploadedBackup
? `Last successful backup ${relativeTime(new Date(lastUploadedBackup.date), this.hass.locale, now, true)} and stored in ${lastUploadedBackup.agent_ids?.length} locations.`
: nextBackupDescription;
const lastBackupDescription = lastSuccessfulBackup
? `Last successful backup ${relativeTime(new Date(lastSuccessfulBackup.date), this.hass.locale, now, true)} and stored in ${lastSuccessfulBackup.agent_ids?.length} locations.`
: "You have no successful backups.";
if (lastAttempt && lastAttempt > (lastCompletedBackupDate || 0)) {
const lastAttemptDescription = `The last automatic backup triggered ${relativeTime(lastAttempt, this.hass.locale, now, true)} wasn't successful.`;
return html` return html`
<ha-backup-summary-card <ha-backup-summary-card
heading="Last automatic backup failed" heading="Last automatic backup failed"
@ -108,29 +124,30 @@ class HaBackupOverviewBackups extends LitElement {
<ha-md-list> <ha-md-list>
<ha-md-list-item> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">${lastAttemptDescription}</span> <span slot="headline">${description}</span>
</ha-md-list-item> </ha-md-list-item>
<ha-md-list-item> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline">${lastBackupDescription}</span> <span slot="headline">${secondaryDescription}</span>
</ha-md-list-item> </ha-md-list-item>
</ha-md-list> </ha-md-list>
</ha-backup-summary-card> </ha-backup-summary-card>
`; `;
} }
const nextBackupDescription = this._nextBackupDescription( // If no backups yet, show warning
this.config.schedule.state if (!lastBackup) {
); const description = "You have no automatic backups yet.";
if (!lastSuccessfulBackup) {
return html` return html`
<ha-backup-summary-card <ha-backup-summary-card
heading="No automatic backup available" heading="No automatic backup available"
description="You have no automatic backups yet."
status="warning" status="warning"
> >
<ha-md-list> <ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">${description}</span>
</ha-md-list-item>
<ha-md-list-item> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline">${nextBackupDescription}</span> <span slot="headline">${nextBackupDescription}</span>
@ -140,10 +157,41 @@ class HaBackupOverviewBackups extends LitElement {
`; `;
} }
const lastBackupDate = new Date(lastBackup.date);
// If last backup
if (lastBackup.failed_agent_ids?.length) {
const description = `The last automatic backup created ${relativeTime(lastBackupDate, this.hass.locale, now, true)} wasn't stored in all locations.`;
const lastUploadedBackup = this._lastUploadedBackup(this.backups);
const secondaryDescription = lastUploadedBackup
? `Last successful backup ${relativeTime(new Date(lastUploadedBackup.date), this.hass.locale, now, true)} and stored in ${lastUploadedBackup.agent_ids?.length} locations.`
: nextBackupDescription;
return html`
<ha-backup-summary-card
heading="Last automatic backup failed"
status="error"
>
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">${description}</span>
</ha-md-list-item>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline">${secondaryDescription}</span>
</ha-md-list-item>
</ha-md-list>
</ha-backup-summary-card>
`;
}
const description = `Last successful backup ${relativeTime(lastBackupDate, this.hass.locale, now, true)} and stored in ${lastBackup.agent_ids?.length} locations.`;
const numberOfDays = differenceInDays( const numberOfDays = differenceInDays(
// Subtract a few hours to avoid showing as overdue if it's just a few hours (e.g. daylight saving) // Subtract a few hours to avoid showing as overdue if it's just a few hours (e.g. daylight saving)
addHours(now, -OVERDUE_MARGIN_HOURS), addHours(now, -OVERDUE_MARGIN_HOURS),
new Date(lastSuccessfulBackup.date) lastBackupDate
); );
const isOverdue = const isOverdue =
@ -160,7 +208,7 @@ class HaBackupOverviewBackups extends LitElement {
<ha-md-list> <ha-md-list>
<ha-md-list-item> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">${lastBackupDescription}</span> <span slot="headline">${description}</span>
</ha-md-list-item> </ha-md-list-item>
<ha-md-list-item> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
@ -170,12 +218,13 @@ class HaBackupOverviewBackups extends LitElement {
</ha-backup-summary-card> </ha-backup-summary-card>
`; `;
} }
return html` return html`
<ha-backup-summary-card heading=${`Backed up`} status="success"> <ha-backup-summary-card heading=${`Backed up`} status="success">
<ha-md-list> <ha-md-list>
<ha-md-list-item> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline">${lastBackupDescription}</span> <span slot="headline">${description}</span>
</ha-md-list-item> </ha-md-list-item>
<ha-md-list-item> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>

View File

@ -198,7 +198,7 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
<p> <p>
${this.hass.connected ${this.hass.connected
? this._restoreState() ? this._restoreState()
: "Restarting Home Asssistant"} : "Restarting Home Assistant"}
</p> </p>
</div>`; </div>`;
} }

View File

@ -1,6 +1,12 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import type { BackupConfig, BackupContent } from "../../../data/backup";
import {
compareAgents,
fetchBackupConfig,
fetchBackupInfo,
} from "../../../data/backup";
import type { ManagerStateEvent } from "../../../data/backup_manager"; import type { ManagerStateEvent } from "../../../data/backup_manager";
import { import {
DEFAULT_MANAGER_STATE, DEFAULT_MANAGER_STATE,
@ -15,12 +21,6 @@ import type { HomeAssistant } from "../../../types";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import "./ha-config-backup-backups"; import "./ha-config-backup-backups";
import "./ha-config-backup-overview"; import "./ha-config-backup-overview";
import type { BackupConfig, BackupContent } from "../../../data/backup";
import {
compareAgents,
fetchBackupConfig,
fetchBackupInfo,
} from "../../../data/backup";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@ -47,13 +47,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this._fetching = true; this._fetchAll();
Promise.all([this._fetchBackupInfo(), this._fetchBackupConfig()]).finally(
() => {
this._fetching = false;
}
);
this.addEventListener("ha-refresh-backup-info", () => { this.addEventListener("ha-refresh-backup-info", () => {
this._fetchBackupInfo(); this._fetchBackupInfo();
}); });
@ -62,6 +56,15 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
}); });
} }
private _fetchAll() {
this._fetching = true;
Promise.all([this._fetchBackupInfo(), this._fetchBackupConfig()]).finally(
() => {
this._fetching = false;
}
);
}
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (this.hasUpdated) { if (this.hasUpdated) {
@ -128,11 +131,16 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
public hassSubscribe(): Promise<UnsubscribeFunc>[] { public hassSubscribe(): Promise<UnsubscribeFunc>[] {
return [ return [
subscribeBackupEvents(this.hass!, (event) => { subscribeBackupEvents(this.hass!, (event) => {
const curState = this._manager.manager_state;
this._manager = event; this._manager = event;
if (
event.manager_state === "idle" &&
event.manager_state !== curState
) {
this._fetchAll();
}
if ("state" in event) { if ("state" in event) {
if (event.state === "completed" || event.state === "failed") {
this._fetchBackupInfo();
}
if (event.state === "failed") { if (event.state === "failed") {
let message = ""; let message = "";
switch (this._manager.manager_state) { switch (this._manager.manager_state) {

View File

@ -749,7 +749,9 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
${Array.isArray(this._filters.config_entry?.value) && ${Array.isArray(this._filters.config_entry?.value) &&
this._filters.config_entry?.value.length this._filters.config_entry?.value.length
? html`<ha-alert slot="filter-pane"> ? html`<ha-alert slot="filter-pane">
Filtering by config entry ${this.hass.localize(
"ui.panel.config.devices.filtering_by_config_entry"
)}
${this.entries?.find( ${this.entries?.find(
(entry) => (entry) =>
entry.entry_id === this._filters.config_entry!.value![0] entry.entry_id === this._filters.config_entry!.value![0]

View File

@ -904,7 +904,9 @@ ${
Array.isArray(this._filters.config_entry) && Array.isArray(this._filters.config_entry) &&
this._filters.config_entry?.length this._filters.config_entry?.length
? html`<ha-alert slot="filter-pane"> ? html`<ha-alert slot="filter-pane">
Filtering by config entry ${this.hass.localize(
"ui.panel.config.entities.picker.filtering_by_config_entry"
)}
${this._entries?.find( ${this._entries?.find(
(entry) => entry.entry_id === this._filters.config_entry![0] (entry) => entry.entry_id === this._filters.config_entry![0]
)?.title || this._filters.config_entry[0]} )?.title || this._filters.config_entry[0]}

View File

@ -555,7 +555,7 @@ class AddIntegrationDialog extends LitElement {
if (integration.integrations) { if (integration.integrations) {
let domains = integration.domains || []; let domains = integration.domains || [];
if (integration.domain === "apple") { if (integration.domain === "apple") {
// we show discoverd homekit devices in their own brand section, dont show them at apple // we show discovered homekit devices in their own brand section, dont show them in apple
domains = domains.filter((domain) => domain !== "homekit_controller"); domains = domains.filter((domain) => domain !== "homekit_controller");
} }
this._fetchFlowsInProgress(domains); this._fetchFlowsInProgress(domains);

View File

@ -864,7 +864,7 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
} }
if (integration?.supported_by) { if (integration?.supported_by) {
// Integration is a alias, so we can just create a flow // Integration is an alias, so we can just create a flow
const localize = await this.hass.loadBackendTranslation( const localize = await this.hass.loadBackendTranslation(
"title", "title",
domain, domain,

View File

@ -18,8 +18,8 @@ import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import type { LovelaceCard, LovelaceGridOptions } from "../../types"; import type { LovelaceCard, LovelaceGridOptions } from "../../types";
import type { EnergySankeyCardConfig } from "../types"; import type { EnergySankeyCardConfig } from "../types";
import "../../../../components/chart/sankey-chart"; import "../../../../components/chart/ha-sankey-chart";
import type { Link, Node } from "../../../../components/chart/sankey-chart"; import type { Link, Node } from "../../../../components/chart/ha-sankey-chart";
import { getGraphColorByIndex } from "../../../../common/color/colors"; import { getGraphColorByIndex } from "../../../../common/color/colors";
import { formatNumber } from "../../../../common/number/format_number"; import { formatNumber } from "../../../../common/number/format_number";
@ -399,13 +399,13 @@ class HuiEnergySankeyCard
<ha-card .header=${this._config.title}> <ha-card .header=${this._config.title}>
<div class="card-content"> <div class="card-content">
${hasData ${hasData
? html`<sankey-chart ? html`<ha-sankey-chart
.data=${{ nodes, links }} .data=${{ nodes, links }}
.vertical=${this._config.layout === "vertical"} .vertical=${this._config.layout === "vertical"}
.loadingText=${this.hass.localize( .loadingText=${this.hass.localize(
"ui.panel.lovelace.cards.energy.loading" "ui.panel.lovelace.cards.energy.loading"
)} )}
></sankey-chart>` ></ha-sankey-chart>`
: html`${this.hass.localize( : html`${this.hass.localize(
"ui.panel.lovelace.cards.energy.no_data_period" "ui.panel.lovelace.cards.energy.no_data_period"
)}`} )}`}

View File

@ -78,7 +78,7 @@ function checkStateCondition(
: UNAVAILABLE; : UNAVAILABLE;
let value = condition.state ?? condition.state_not; let value = condition.state ?? condition.state_not;
// Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now) // Handle entity_id, UI should be updated for conditional card (filters does not have UI for now)
if (Array.isArray(value)) { if (Array.isArray(value)) {
const entityValues = value const entityValues = value
.map((v) => getValueFromEntityId(hass, v)) .map((v) => getValueFromEntityId(hass, v))
@ -106,7 +106,7 @@ function checkStateNumericCondition(
let above = condition.above; let above = condition.above;
let below = condition.below; let below = condition.below;
// Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now) // Handle entity_id, UI should be updated for conditional card (filters does not have UI for now)
if (typeof above === "string") { if (typeof above === "string") {
above = getValueFromEntityId(hass, above) ?? above; above = getValueFromEntityId(hass, above) ?? above;
} }

View File

@ -218,7 +218,7 @@ const _lazyCreate = <T extends keyof CreateElementConfigTypes>(
// @ts-ignore // @ts-ignore
element.setConfig(config); element.setConfig(config);
} catch (err: any) { } catch (err: any) {
// We let it rebuild and the error wil be handled by _createElement // We let it rebuild and the error will be handled by _createElement
fireEvent(element, "ll-rebuild"); fireEvent(element, "ll-rebuild");
} }
}); });

View File

@ -49,7 +49,7 @@ export class HuiServiceButtonElement
} }
if (!this._service) { if (!this._service) {
throw Error("Action does not have a action name"); throw Error("Action does not have an action name");
} }
this._config = config; this._config = config;

View File

@ -231,7 +231,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
// An other create columns is started, abort this one // An other create columns is started, abort this one
return; return;
} }
// Calculate in wich column the card should go based on the size and the cards already in there // Calculate in which column the card should go based on the size and the cards already in there
this._addCardToColumn( this._addCardToColumn(
columnElements[getColumnIndex(columnSizes, cardSize as number)], columnElements[getColumnIndex(columnSizes, cardSize as number)],
index, index,

View File

@ -298,7 +298,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
} }
const resources = await getHassTranslationsPre109(this.hass!, language); const resources = await getHassTranslationsPre109(this.hass!, language);
// Ignore the repsonse if user switched languages before we got response // Ignore the response if user switched languages before we got response
if (this.hass!.language !== language) { if (this.hass!.language !== language) {
return this.hass!.localize; return this.hass!.localize;
} }
@ -359,7 +359,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
configFlow configFlow
); );
// Ignore the repsonse if user switched languages before we got response // Ignore the response if user switched languages before we got response
if (this.hass!.language !== language) { if (this.hass!.language !== language) {
return this.hass!.localize; return this.hass!.localize;
} }

View File

@ -1054,7 +1054,7 @@
}, },
"end_after": { "end_after": {
"label": "End after", "label": "End after",
"ocurrences": "ocurrences" "ocurrences": "occurrences"
} }
}, },
"rrule": { "rrule": {
@ -1269,7 +1269,6 @@
"clear_skipped": "Clear skipped", "clear_skipped": "Clear skipped",
"install": "Install", "install": "Install",
"update": "Update", "update": "Update",
"create_backup": "Create backup before updating",
"auto_update_enabled_title": "Can not skip version", "auto_update_enabled_title": "Can not skip version",
"auto_update_enabled_text": "Automatic updates for this item have been enabled; skipping it is, therefore, unavailable. You can either install this update now or wait for Home Assistant to do it automatically." "auto_update_enabled_text": "Automatic updates for this item have been enabled; skipping it is, therefore, unavailable. You can either install this update now or wait for Home Assistant to do it automatically."
}, },
@ -2437,7 +2436,7 @@
}, },
"create_helper": "Create helper", "create_helper": "Create helper",
"no_helpers": "Looks like you don't have any helpers yet!", "no_helpers": "Looks like you don't have any helpers yet!",
"search": "Search {number} helpers", "search": "Search {number} {number, plural,\n one {helper}\n other {helpers}\n}",
"error_information": "Error information" "error_information": "Error information"
}, },
"dialog": { "dialog": {
@ -2496,7 +2495,7 @@
"enable_remote": "[%key:ui::common::enable%]", "enable_remote": "[%key:ui::common::enable%]",
"internal_url_automatic": "Automatic", "internal_url_automatic": "Automatic",
"internal_url_https_error_title": "Invalid local network URL", "internal_url_https_error_title": "Invalid local network URL",
"internal_url_https_error_description": "You have configured an HTTPS certificate in Home Assistant. This means that your internal URL needs to be set to a domain covered by the certficate.", "internal_url_https_error_description": "You have configured an HTTPS certificate in Home Assistant. This means that your internal URL needs to be set to a domain covered by the certificate.",
"internal_url_automatic_description": "Use the configured network settings", "internal_url_automatic_description": "Use the configured network settings",
"internal_url_placeholder": "http://<some IP address>:8123" "internal_url_placeholder": "http://<some IP address>:8123"
}, },
@ -2962,7 +2961,7 @@
"assign_category": "Assign category", "assign_category": "Assign category",
"no_category_support": "You can't assign a category to this automation", "no_category_support": "You can't assign a category to this automation",
"no_category_entity_reg": "To assign a category to an automation it needs to have a unique ID.", "no_category_entity_reg": "To assign a category to an automation it needs to have a unique ID.",
"search": "Search {number} automations", "search": "Search {number} {number, plural,\n one {automation}\n other {automations}\n}",
"headers": { "headers": {
"toggle": "Enable/disable", "toggle": "Enable/disable",
"name": "Name", "name": "Name",
@ -3877,13 +3876,13 @@
}, },
"edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]", "edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]",
"assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]", "assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]",
"no_category_support": "You can't assign an category to this script", "no_category_support": "You can't assign a category to this script",
"no_category_entity_reg": "To assign an category to an script it needs to have a unique ID.", "no_category_entity_reg": "To assign a category to a script it needs to have a unique ID.",
"delete": "[%key:ui::common::delete%]", "delete": "[%key:ui::common::delete%]",
"duplicate": "[%key:ui::common::duplicate%]", "duplicate": "[%key:ui::common::duplicate%]",
"empty_header": "Create your first script", "empty_header": "Create your first script",
"empty_text": "A script is a sequence of actions that can be run from a dashboard, an automation, or be triggered by voice. For example, a ''Wake-up routine''' script that gradually turns on the light in the bedroom and opens the blinds after a delay.", "empty_text": "A script is a sequence of actions that can be run from a dashboard, an automation, or be triggered by voice. For example, a ''Wake-up routine''' script that gradually turns on the light in the bedroom and opens the blinds after a delay.",
"search": "Search {number} scripts", "search": "Search {number} {number, plural,\n one {script}\n other {scripts}\n}",
"migrate_script": "Migrate script?", "migrate_script": "Migrate script?",
"migrate_script_description": "You can migrate this script, so it can be edited from the UI. After it is migrated and you have saved it, you will have to manually delete your old script from your configuration. Do you want to migrate this script?" "migrate_script_description": "You can migrate this script, so it can be edited from the UI. After it is migrated and you have saved it, you will have to manually delete your old script from your configuration. Do you want to migrate this script?"
}, },
@ -3993,11 +3992,11 @@
}, },
"edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]", "edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]",
"assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]", "assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]",
"no_category_support": "You can't assign an category to this scene", "no_category_support": "You can't assign a category to this scene",
"no_category_entity_reg": "To assign an category to an scene it needs to have a unique ID.", "no_category_entity_reg": "To assign a category to an scene it needs to have a unique ID.",
"empty_header": "Create your first scene", "empty_header": "Create your first scene",
"empty_text": "Scenes capture entities' states, so you can re-experience the same scene later on. For example, a ''Watching TV'' scene that dims the living room lights, sets a warm white color and turns on the TV.", "empty_text": "Scenes capture entities' states, so you can re-experience the same scene later on. For example, a ''Watching TV'' scene that dims the living room lights, sets a warm white color and turns on the TV.",
"search": "Search {number} scenes" "search": "Search {number} {number, plural,\n one {scene}\n other {scenes}\n}"
}, },
"editor": { "editor": {
"review_mode": "Review Mode", "review_mode": "Review Mode",
@ -4241,6 +4240,7 @@
"add_device": "Add device", "add_device": "Add device",
"caption": "Devices", "caption": "Devices",
"description": "Manage configured devices", "description": "Manage configured devices",
"filtering_by_config_entry": "[%key:ui::panel::config::entities::picker::filtering_by_config_entry%]",
"device_info": "{type} info", "device_info": "{type} info",
"edit_settings": "Edit settings", "edit_settings": "Edit settings",
"unnamed_device": "Unnamed {type}", "unnamed_device": "Unnamed {type}",
@ -4369,7 +4369,7 @@
"confirm_delete_integration": "Are you sure you want to remove this device from {integration}?", "confirm_delete_integration": "Are you sure you want to remove this device from {integration}?",
"error_delete": "Error deleting device", "error_delete": "Error deleting device",
"picker": { "picker": {
"search": "Search {number} devices", "search": "Search {number} {number, plural,\n one {device}\n other {devices}\n}",
"state": "Status", "state": "Status",
"bulk_actions": { "bulk_actions": {
"move_area": "Move to area", "move_area": "Move to area",
@ -4385,8 +4385,9 @@
"header": "Entities", "header": "Entities",
"introduction": "Home Assistant keeps a registry of every entity it has ever seen that can be uniquely identified. Each of these entities will have an entity ID assigned which will be reserved for just this entity.", "introduction": "Home Assistant keeps a registry of every entity it has ever seen that can be uniquely identified. Each of these entities will have an entity ID assigned which will be reserved for just this entity.",
"introduction2": "Use the entity registry to override the name, change the entity ID or remove the entry from Home Assistant.", "introduction2": "Use the entity registry to override the name, change the entity ID or remove the entry from Home Assistant.",
"search": "Search {number} entities", "search": "Search {number} {number, plural,\n one {entity}\n other {entities}\n}",
"unnamed_entity": "Unnamed entity", "unnamed_entity": "Unnamed entity",
"filtering_by_config_entry": "Filtering by config entry",
"status": { "status": {
"available": "Available", "available": "Available",
"unavailable": "Unavailable", "unavailable": "Unavailable",
@ -4583,7 +4584,7 @@
"application_credentials": { "application_credentials": {
"delete_title": "Application credentials", "delete_title": "Application credentials",
"delete_prompt": "Would you like to also delete Application Credentials for this integration?", "delete_prompt": "Would you like to also delete Application Credentials for this integration?",
"delete_detail": "If you delete them, you will need to enter credentials when setting up the integration again. If you keep them, they will be used automatically when setting up the integration again or may be acccessed from the Application Credentials menu.", "delete_detail": "If you delete them, you will need to enter credentials when setting up the integration again. If you keep them, they will be used automatically when setting up the integration again or may be accessed from the Application Credentials menu.",
"delete_error_title": "Deleting application credentials failed", "delete_error_title": "Deleting application credentials failed",
"dismiss": "Keep", "dismiss": "Keep",
"learn_more": "Learn more about application credentials" "learn_more": "Learn more about application credentials"
@ -5510,7 +5511,7 @@
"ping_node": { "ping_node": {
"title": "Ping a Matter device", "title": "Ping a Matter device",
"introduction": "Perform a (server-side) ping on your Matter device on all its (known) IP-addresses.", "introduction": "Perform a (server-side) ping on your Matter device on all its (known) IP-addresses.",
"battery_device_warning": "Note that especially for battery powered devices this can take a a while. You may need to wake up battery powered devices before starting the pinging to speed up the process. Refer to your device's manual for instructions on how to wake the device.", "battery_device_warning": "Note that especially for battery powered devices this can take a while. You may need to wake up battery powered devices before starting the pinging to speed up the process. Refer to your device's manual for instructions on how to wake the device.",
"start_ping": "Start ping", "start_ping": "Start ping",
"in_progress": "The device is being pinged. This may take some time.", "in_progress": "The device is being pinged. This may take some time.",
"ping_failed": "The device ping failed. Additional information may be available in the logs.", "ping_failed": "The device ping failed. Additional information may be available in the logs.",
@ -5679,7 +5680,7 @@
}, },
"version": { "version": {
"title": "Samba/Windows (CIFS) version", "title": "Samba/Windows (CIFS) version",
"description": "This choses the version of the protocol to use" "description": "This chooses the version of the protocol to use"
}, },
"username": { "username": {
"title": "Username", "title": "Username",
@ -7999,8 +8000,7 @@
"update_available": { "update_available": {
"update_name": "Update {name}", "update_name": "Update {name}",
"open_release_notes": "Open release notes", "open_release_notes": "Open release notes",
"create_backup": "Create backup before updating", "description": "You have {version} installed. Press update to update to version {newest_version}",
"description": "You have {version} installed. Click update to update to version {newest_version}",
"updating": "Updating {name} to version {version}", "updating": "Updating {name} to version {version}",
"no_update": "No update available for {name}" "no_update": "No update available for {name}"
}, },

View File

@ -42,7 +42,7 @@ export function findAvailableLanguage(language: string) {
return language; return language;
} }
// Perform case-insenstive comparison since browser isn't required to // Perform case-insensitive comparison since browser isn't required to
// report languages with specific cases. // report languages with specific cases.
const langLower = language.toLowerCase(); const langLower = language.toLowerCase();

View File

@ -9,7 +9,7 @@ describe("attributeClassNames", () => {
assert.strictEqual(attributeClassNames(stateObj, attrs), ""); assert.strictEqual(attributeClassNames(stateObj, attrs), "");
}); });
it("Matches no attrbutes", () => { it("Matches no attributes", () => {
const stateObj: any = { const stateObj: any = {
attributes: { attributes: {
other_attr_1: 1, other_attr_1: 1,
@ -19,7 +19,7 @@ describe("attributeClassNames", () => {
assert.strictEqual(attributeClassNames(stateObj, attrs), ""); assert.strictEqual(attributeClassNames(stateObj, attrs), "");
}); });
it("Matches one attrbute", () => { it("Matches one attribute", () => {
const stateObj: any = { const stateObj: any = {
attributes: { attributes: {
other_attr_1: 1, other_attr_1: 1,
@ -30,7 +30,7 @@ describe("attributeClassNames", () => {
assert.strictEqual(attributeClassNames(stateObj, attrs), "has-mock_attr1"); assert.strictEqual(attributeClassNames(stateObj, attrs), "has-mock_attr1");
}); });
it("Matches two attrbutes", () => { it("Matches two attributes", () => {
const stateObj: any = { const stateObj: any = {
attributes: { attributes: {
other_attr_1: 1, other_attr_1: 1,