Compare commits

...

1 Commits

Author SHA1 Message Date
Aidan Timson
275662b4b4 Setup 2026-03-24 18:47:32 +00:00
15 changed files with 776 additions and 3 deletions

View File

@@ -22,6 +22,10 @@ export interface HomeFrontendSystemData {
welcome_banner_dismissed?: boolean;
}
export interface MaintenanceFrontendSystemData {
battery_attention_threshold?: number;
}
declare global {
interface FrontendUserData {
core: CoreFrontendUserData;
@@ -30,6 +34,7 @@ declare global {
interface FrontendSystemData {
core: CoreFrontendSystemData;
home: HomeFrontendSystemData;
maintenance: MaintenanceFrontendSystemData;
}
}

View File

@@ -31,6 +31,7 @@ const COMPONENTS = {
todo: () => import("../panels/todo/ha-panel-todo"),
"media-browser": () =>
import("../panels/media-browser/ha-panel-media-browser"),
maintenance: () => import("../panels/maintenance/ha-panel-maintenance"),
light: () => import("../panels/light/ha-panel-light"),
security: () => import("../panels/security/ha-panel-security"),
climate: () => import("../panels/climate/ha-panel-climate"),

View File

@@ -16,6 +16,7 @@ import {
fetchFrontendSystemData,
saveFrontendSystemData,
type HomeFrontendSystemData,
type MaintenanceFrontendSystemData,
} from "../../data/frontend";
import type { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config/types";
import { mdiHomeAssistant } from "../../resources/home-assistant-logo-svg";
@@ -46,6 +47,8 @@ class PanelHome extends LitElement {
@state() private _config: FrontendSystemData["home"] = {};
@state() private _maintenanceConfig: MaintenanceFrontendSystemData = {};
@state() private _extraActionItems?: ExtraActionItem[];
private get _showBanner(): boolean {
@@ -122,15 +125,18 @@ class PanelHome extends LitElement {
private async _setup() {
this._updateExtraActionItems();
try {
const [_, data] = await Promise.all([
const [_, homeData, maintenanceData] = await Promise.all([
this.hass.loadFragmentTranslation("lovelace"),
fetchFrontendSystemData(this.hass.connection, "home"),
fetchFrontendSystemData(this.hass.connection, "maintenance"),
]);
this._config = data || {};
this._config = homeData || {};
this._maintenanceConfig = maintenanceData || {};
} catch (err) {
// eslint-disable-next-line no-console
console.error("Failed to load favorites:", err);
this._config = {};
this._maintenanceConfig = {};
}
this._setLovelace();
}
@@ -317,6 +323,8 @@ class PanelHome extends LitElement {
strategy: {
type: "home",
favorite_entities: this._config.favorite_entities,
battery_attention_threshold:
this._maintenanceConfig.battery_attention_threshold,
home_panel: true,
},
};

View File

@@ -27,6 +27,7 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { countMaintenanceDevicesNeedingAttention } from "../../maintenance/maintenance-battery-data";
import {
getSummaryLabel,
HOME_SUMMARIES_FILTERS,
@@ -42,6 +43,7 @@ const COLORS: Record<HomeSummary, string> = {
climate: "deep-orange",
security: "blue-grey",
media_players: "blue",
maintenance: "amber",
energy: "amber",
};
@@ -257,6 +259,21 @@ export class HuiHomeSummaryCard
const totalConsumption = consumption.total.used_total;
return formatConsumptionShort(this.hass, totalConsumption, "kWh");
}
case "maintenance": {
const count = countMaintenanceDevicesNeedingAttention(
this.hass,
this._config.attention_threshold
);
return count > 0
? this.hass.localize(
"ui.card.home-summary.count_devices_needing_attention",
{ count }
)
: this.hass.localize(
"ui.card.home-summary.no_devices_needing_attention"
);
}
}
return "";
}

View File

@@ -664,6 +664,7 @@ export interface HeadingCardConfig extends LovelaceCardConfig {
export interface HomeSummaryCard extends LovelaceCardConfig {
summary: HomeSummary;
attention_threshold?: number;
vertical?: boolean;
tap_action?: ActionConfig;
hold_action?: ActionConfig;

View File

@@ -56,6 +56,8 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
"home-area": () => import("./home/home-area-view-strategy"),
"home-other-devices": () =>
import("./home/home-other-devices-view-strategy"),
maintenance: () =>
import("../../maintenance/strategies/maintenance-view-strategy"),
light: () => import("../../light/strategies/light-view-strategy"),
security: () => import("../../security/strategies/security-view-strategy"),
climate: () => import("../../climate/strategies/climate-view-strategy"),

View File

@@ -9,6 +9,7 @@ export const HOME_SUMMARIES = [
"climate",
"security",
"media_players",
"maintenance",
"energy",
] as const;
@@ -19,6 +20,7 @@ export const HOME_SUMMARIES_ICONS: Record<HomeSummary, string> = {
climate: "mdi:home-thermometer",
security: "mdi:security",
media_players: "mdi:multimedia",
maintenance: "mdi:battery-heart-variant",
energy: "mdi:lightning-bolt",
};
@@ -27,6 +29,7 @@ export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
climate: climateEntityFilters,
security: securityEntityFilters,
media_players: [{ domain: "media_player", entity_category: "none" }],
maintenance: [],
energy: [], // Uses energy collection data
};

View File

@@ -16,6 +16,7 @@ import type { HomeOverviewViewStrategyConfig } from "./home-overview-view-strate
export interface HomeDashboardStrategyConfig {
type: "home";
favorite_entities?: string[];
battery_attention_threshold?: number;
home_panel?: boolean;
}
@@ -93,6 +94,7 @@ export class HomeDashboardStrategy extends ReactiveElement {
strategy: {
type: "home-overview",
favorite_entities: config.favorite_entities,
battery_attention_threshold: config.battery_attention_threshold,
home_panel: config.home_panel,
} satisfies HomeOverviewViewStrategyConfig,
},

View File

@@ -27,6 +27,7 @@ import type {
TileCardConfig,
UpdatesCardConfig,
} from "../../cards/types";
import { getMaintenanceBatteryDevices } from "../../../maintenance/maintenance-battery-data";
import {
LARGE_SCREEN_CONDITION,
SMALL_SCREEN_CONDITION,
@@ -38,6 +39,7 @@ import { OTHER_DEVICES_FILTERS } from "./helpers/other-devices-filters";
export interface HomeOverviewViewStrategyConfig {
type: "home-overview";
favorite_entities?: string[];
battery_attention_threshold?: number;
home_panel?: boolean;
}
@@ -232,6 +234,10 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
const hasSecurity =
hass.panels.security &&
findEntities(allEntities, securityFilters).length > 0;
const hasMaintenance =
hass.panels.maintenance &&
getMaintenanceBatteryDevices(hass, config.battery_attention_threshold)
.length > 0;
const weatherFilter = generateEntityFilter(hass, {
domain: "weather",
@@ -315,6 +321,16 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
navigation_path: "media-players",
},
} satisfies HomeSummaryCard),
hasMaintenance &&
({
type: "home-summary",
summary: "maintenance",
attention_threshold: config.battery_attention_threshold,
tap_action: {
action: "navigate",
navigation_path: "/maintenance?historyBack=1",
},
} satisfies HomeSummaryCard),
weatherEntity &&
({
type: "tile",

View File

@@ -0,0 +1,178 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-dialog";
import "../../../components/ha-form/ha-form";
import type { HaFormSchema } from "../../../components/ha-form/types";
import type { MaintenanceFrontendSystemData } from "../../../data/frontend";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { DEFAULT_BATTERY_ATTENTION_THRESHOLD } from "../maintenance-battery-data";
import type { EditMaintenanceDialogParams } from "./show-dialog-edit-maintenance";
const THRESHOLD_SCHEMA = [
{
name: "battery_attention_threshold",
selector: {
number: {
min: 0,
max: 100,
mode: "slider",
slider_ticks: true,
},
},
},
] as const satisfies HaFormSchema[];
@customElement("dialog-edit-maintenance")
export class DialogEditMaintenance
extends LitElement
implements HassDialog<EditMaintenanceDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: EditMaintenanceDialogParams;
@state() private _config?: MaintenanceFrontendSystemData;
@state() private _open = false;
@state() private _submitting = false;
public showDialog(params: EditMaintenanceDialogParams): void {
this._params = params;
this._config = { ...params.config };
this._open = true;
}
public closeDialog(): boolean {
this._open = false;
return true;
}
private _dialogClosed(): void {
this._params = undefined;
this._config = undefined;
this._submitting = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-dialog
.hass=${this.hass}
.open=${this._open}
.headerTitle=${this.hass.localize("ui.panel.maintenance.editor.title")}
prevent-scrim-close
@closed=${this._dialogClosed}
>
<p class="description">
${this.hass.localize("ui.panel.maintenance.editor.description")}
</p>
<ha-form
autofocus
.hass=${this.hass}
.data=${{
battery_attention_threshold:
this._config?.battery_attention_threshold ??
DEFAULT_BATTERY_ATTENTION_THRESHOLD,
}}
.schema=${THRESHOLD_SCHEMA}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
@value-changed=${this._valueChanged}
></ha-form>
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._save}
.disabled=${this._submitting}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-dialog>
`;
}
private _valueChanged(ev: CustomEvent): void {
const threshold = ev.detail.value.battery_attention_threshold as number;
this._config = {
battery_attention_threshold:
threshold === DEFAULT_BATTERY_ATTENTION_THRESHOLD
? undefined
: threshold,
};
}
private _computeLabel = (schema: HaFormSchema) =>
schema.name === "battery_attention_threshold"
? this.hass.localize(
"ui.panel.maintenance.editor.battery_attention_threshold"
)
: "";
private _computeHelper = (schema: HaFormSchema) =>
schema.name === "battery_attention_threshold"
? this.hass.localize(
"ui.panel.maintenance.editor.battery_attention_threshold_helper"
)
: "";
private async _save(): Promise<void> {
if (!this._params || !this._config) {
return;
}
this._submitting = true;
try {
await this._params.saveConfig(this._config);
this.closeDialog();
} finally {
this._submitting = false;
}
}
static styles = [
haStyleDialog,
css`
ha-dialog {
--dialog-content-padding: var(--ha-space-6);
}
.description {
margin: 0 0 var(--ha-space-4) 0;
color: var(--secondary-text-color);
}
ha-form {
display: block;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"dialog-edit-maintenance": DialogEditMaintenance;
}
}

View File

@@ -0,0 +1,21 @@
import { fireEvent } from "../../../common/dom/fire_event";
import type { MaintenanceFrontendSystemData } from "../../../data/frontend";
export interface EditMaintenanceDialogParams {
config: MaintenanceFrontendSystemData;
saveConfig: (config: MaintenanceFrontendSystemData) => Promise<void>;
}
export const loadEditMaintenanceDialog = () =>
import("./dialog-edit-maintenance");
export const showEditMaintenanceDialog = (
element: HTMLElement,
params: EditMaintenanceDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-edit-maintenance",
dialogImport: loadEditMaintenanceDialog,
dialogParams: params,
});
};

View File

@@ -0,0 +1,311 @@
import { mdiPencil } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { goBack } from "../../common/navigate";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-icon-button";
import "../../components/ha-menu-button";
import {
fetchFrontendSystemData,
saveFrontendSystemData,
type MaintenanceFrontendSystemData,
} from "../../data/frontend";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { showToast } from "../../util/toast";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-background";
import "../lovelace/views/hui-view-container";
import { showEditMaintenanceDialog } from "./dialogs/show-dialog-edit-maintenance";
@customElement("ha-panel-maintenance")
class PanelMaintenance extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() private _viewIndex = 0;
@state() private _lovelace?: Lovelace;
@state() private _config: MaintenanceFrontendSystemData = {};
@state() private _searchParms = new URLSearchParams(window.location.search);
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._setup();
return;
}
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass && oldHass.localize !== this.hass.localize) {
this._setLovelace();
return;
}
if (oldHass && this.hass) {
if (
oldHass.entities !== this.hass.entities ||
oldHass.devices !== this.hass.devices ||
oldHass.areas !== this.hass.areas ||
oldHass.floors !== this.hass.floors ||
oldHass.panels !== this.hass.panels
) {
if (this.hass.config.state === "RUNNING") {
this._debounceRegistriesChanged();
return;
}
}
if (
this.hass.config.state === "RUNNING" &&
oldHass.config.state !== "RUNNING"
) {
this._setup();
}
}
}
private async _setup() {
await this.hass.loadFragmentTranslation("lovelace");
try {
this._config =
(await fetchFrontendSystemData(this.hass.connection, "maintenance")) ||
{};
} catch (_err) {
this._config = {};
}
this._setLovelace();
}
private _debounceRegistriesChanged = debounce(
() => this._registriesChanged(),
200
);
private _registriesChanged = async () => {
this._setLovelace();
};
private _back(ev) {
ev.stopPropagation();
goBack();
}
protected render() {
return html`
<div class="header ${classMap({ narrow: this.narrow })}">
<div class="toolbar">
${this._searchParms.has("historyBack")
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
<div class="main-title">
${this.hass.localize("panel.maintenance")}
</div>
<ha-icon-button
.label=${this.hass.localize("ui.panel.maintenance.editor.title")}
.path=${mdiPencil}
@click=${this._editMaintenance}
></ha-icon-button>
</div>
</div>
${this._lovelace
? html`
<hui-view-container .hass=${this.hass}>
<hui-view-background .hass=${this.hass}></hui-view-background>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view>
</hui-view-container>
`
: nothing}
`;
}
private async _setLovelace() {
const rawViewConfig: LovelaceStrategyViewConfig = {
strategy: {
type: "maintenance",
battery_attention_threshold: this._config.battery_attention_threshold,
},
};
const viewConfig = await generateLovelaceViewStrategy(
rawViewConfig,
this.hass
);
const config = { views: [viewConfig] };
const rawConfig = { views: [rawViewConfig] };
if (deepEqual(config, this._lovelace?.config)) {
return;
}
this._lovelace = {
config,
rawConfig,
editMode: false,
urlPath: "maintenance",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
private _editMaintenance = () => {
showEditMaintenanceDialog(this, {
config: this._config,
saveConfig: async (config) => {
await this._saveConfig(config);
},
});
};
private async _saveConfig(
config: MaintenanceFrontendSystemData
): Promise<void> {
try {
await saveFrontendSystemData(this.hass.connection, "maintenance", config);
this._config = config;
} catch (_err) {
showToast(this, {
message: this.hass.localize("ui.panel.maintenance.editor.save_failed"),
duration: 0,
dismissable: true,
});
return;
}
showToast(this, {
message: this.hass.localize("ui.common.successfully_saved"),
});
this._setLovelace();
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
.header {
background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white);
position: fixed;
top: 0;
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-right,
0px
)
);
padding-top: var(--safe-area-inset-top);
z-index: 4;
display: flex;
flex-direction: row;
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
backdrop-filter: var(--app-header-backdrop-filter, none);
padding-right: var(--safe-area-inset-right);
}
:host([narrow]) .header {
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-left,
0px
) - var(--safe-area-inset-right, 0px)
);
padding-left: var(--safe-area-inset-left);
}
:host([scrolled]) .header {
box-shadow: var(
--mdc-top-app-bar-fixed-box-shadow,
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
);
}
.toolbar {
height: var(--header-height);
display: flex;
flex: 1;
align-items: center;
font-size: var(--ha-font-size-xl);
padding: 0 12px;
font-weight: var(--ha-font-weight-normal);
box-sizing: border-box;
border-bottom: var(--app-header-border-bottom, none);
}
:host([narrow]) .toolbar {
padding: 0 4px;
}
.main-title {
margin-inline-start: var(--ha-space-6);
line-height: var(--ha-line-height-normal);
flex-grow: 1;
}
.narrow .main-title {
margin-inline-start: var(--ha-space-2);
}
hui-view-container {
position: relative;
display: flex;
min-height: 100vh;
box-sizing: border-box;
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-panel-maintenance": PanelMaintenance;
}
}

View File

@@ -0,0 +1,104 @@
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeStateName } from "../../common/entity/compute_state_name";
import { generateEntityFilter } from "../../common/entity/entity_filter";
import { clamp } from "../../common/number/clamp";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import {
findBatteryEntity,
type EntityRegistryDisplayEntry,
} from "../../data/entity/entity_registry";
import type { HomeAssistant } from "../../types";
export const DEFAULT_BATTERY_ATTENTION_THRESHOLD = 30;
export interface MaintenanceBatteryDevice {
deviceId: string;
deviceName: string;
entityId: string;
level: number;
needsAttention: boolean;
}
export const normalizeBatteryAttentionThreshold = (
threshold?: number
): number =>
typeof threshold === "number" && !Number.isNaN(threshold)
? clamp(Math.round(threshold), 0, 100)
: DEFAULT_BATTERY_ATTENTION_THRESHOLD;
export const getMaintenanceBatteryDevices = (
hass: HomeAssistant,
attentionThreshold?: number
): MaintenanceBatteryDevice[] => {
const normalizedThreshold =
normalizeBatteryAttentionThreshold(attentionThreshold);
const batteryFilter = generateEntityFilter(hass, {
domain: "sensor",
device_class: "battery",
});
const entitiesByDevice: Record<string, EntityRegistryDisplayEntry[]> = {};
for (const entry of Object.values(hass.entities)) {
if (!entry.device_id || !(entry.entity_id in hass.states)) {
continue;
}
if (!batteryFilter(entry.entity_id)) {
continue;
}
if (!(entry.device_id in entitiesByDevice)) {
entitiesByDevice[entry.device_id] = [];
}
entitiesByDevice[entry.device_id].push(entry);
}
return Object.entries(entitiesByDevice)
.flatMap(([deviceId, entities]) => {
const batteryEntity = findBatteryEntity(hass, entities);
if (!batteryEntity) {
return [];
}
const stateObj = hass.states[batteryEntity.entity_id];
const level = Number(stateObj?.state);
if (!stateObj || !Number.isFinite(level)) {
return [];
}
const device = hass.devices[deviceId];
const deviceName =
(device && computeDeviceName(device)) ||
computeStateName(stateObj) ||
hass.localize("ui.panel.lovelace.strategy.home.unnamed_device");
return [
{
deviceId,
deviceName,
entityId: batteryEntity.entity_id,
level,
needsAttention: level < normalizedThreshold,
},
];
})
.sort(
(a, b) =>
Number(b.needsAttention) - Number(a.needsAttention) ||
a.level - b.level ||
caseInsensitiveStringCompare(a.deviceName, b.deviceName)
);
};
export const countMaintenanceDevicesNeedingAttention = (
hass: HomeAssistant,
attentionThreshold?: number
): number =>
getMaintenanceBatteryDevices(hass, attentionThreshold).filter(
(device) => device.needsAttention
).length;

View File

@@ -0,0 +1,88 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import type {
EmptyStateCardConfig,
HeadingCardConfig,
TileCardConfig,
} from "../../lovelace/cards/types";
import { getMaintenanceBatteryDevices } from "../maintenance-battery-data";
export interface MaintenanceViewStrategyConfig {
type: "maintenance";
battery_attention_threshold?: number;
}
@customElement("maintenance-view-strategy")
export class MaintenanceViewStrategy extends ReactiveElement {
static async generate(
config: MaintenanceViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const batteryDevices = getMaintenanceBatteryDevices(
hass,
config.battery_attention_threshold
);
if (batteryDevices.length === 0) {
return {
type: "panel",
cards: [
{
type: "empty-state",
icon: "mdi:battery-outline",
icon_color: "primary",
content_only: true,
title: hass.localize("ui.panel.maintenance.empty_title"),
content: hass.localize("ui.panel.maintenance.empty_content"),
} as EmptyStateCardConfig,
],
};
}
const section: LovelaceSectionRawConfig = {
type: "grid",
cards: [
{
type: "heading",
heading: hass.localize("ui.panel.maintenance.devices"),
heading_style: "title",
icon: "mdi:battery-heart-variant",
} satisfies HeadingCardConfig,
...batteryDevices.map(
(device) =>
({
type: "tile",
entity: device.entityId,
name: device.deviceName,
tap_action: {
action: "navigate",
navigation_path: `/config/devices/device/${device.deviceId}`,
},
features: [
{
type: "bar-gauge",
min: 0,
max: 100,
},
],
}) satisfies TileCardConfig
),
],
};
return {
type: "sections",
max_columns: 1,
sections: [section],
};
}
}
declare global {
interface HTMLElementTagNameMap {
"maintenance-view-strategy": MaintenanceViewStrategy;
}
}

View File

@@ -15,6 +15,7 @@
"light": "Lights",
"security": "Security",
"climate": "Climate",
"maintenance": "Maintenance",
"home": "Overview",
"notfound": "Page not found"
},
@@ -214,7 +215,9 @@
"count_alarms_disarmed": "{count} {count, plural,\n one {disarmed}\n other {disarmed}\n}",
"all_secure": "All secure",
"no_media_playing": "No media playing",
"count_media_playing": "{count} {count, plural,\n one {playing}\n other {playing}\n}"
"count_media_playing": "{count} {count, plural,\n one {playing}\n other {playing}\n}",
"count_devices_needing_attention": "{count} {count, plural,\n one {needs attention}\n other {need attention}\n}",
"no_devices_needing_attention": "No devices need attention"
},
"toggle-group": {
"all_off": "All off",
@@ -2442,6 +2445,18 @@
"learn_more": "Learn more"
}
},
"maintenance": {
"devices": "Devices",
"empty_title": "No battery devices found",
"empty_content": "Home Assistant could not find any devices with numeric battery sensors.",
"editor": {
"title": "Edit maintenance dashboard",
"description": "Choose when a battery-powered device should be marked as needing attention.",
"battery_attention_threshold": "Battery attention threshold",
"battery_attention_threshold_helper": "Devices below this battery level are shown as needing attention.",
"save_failed": "Failed to save maintenance dashboard configuration"
}
},
"my": {
"not_supported": "This redirect is not supported by your Home Assistant instance. Check the {link} for the supported redirects and the version they where introduced.",
"component_not_loaded": "This redirect is not supported by your Home Assistant instance. You need the integration {integration} to use this redirect.",
@@ -8187,6 +8202,7 @@
"home": {
"summary_list": {
"media_players": "Media players",
"maintenance": "Maintenance",
"other_devices": "Other devices",
"weather": "Weather",
"energy": "Today's energy"