1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-02-15 07:25:54 +00:00
Files
frontend/src/data/update.ts
Michael e0a9f5a08a Show also not installable updates on update overview page (#28717)
* add "show not installable option" to update page

* split updates by install feature and show always

* fix

* fix "no update" panel

* use `nothing` instead of empty string

* re-add `outlined` to ha-card

* keep title, use different for not-installable updates
2026-01-12 13:18:53 +02:00

267 lines
7.6 KiB
TypeScript

import type {
HassEntities,
HassEntityAttributeBase,
HassEntityBase,
HassEvent,
} from "home-assistant-js-websocket";
import { BINARY_STATE_OFF, BINARY_STATE_ON } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { supportsFeature } from "../common/entity/supports-feature";
import { formatNumber } from "../common/number/format_number";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../types";
import { showToast } from "../util/toast";
import type { EntitySources } from "./entity/entity_sources";
export enum UpdateEntityFeature {
INSTALL = 1,
SPECIFIC_VERSION = 2,
PROGRESS = 4,
BACKUP = 8,
RELEASE_NOTES = 16,
}
interface UpdateEntityAttributes extends HassEntityAttributeBase {
auto_update: boolean | null;
display_precision: number;
installed_version: string | null;
in_progress: boolean;
latest_version: string | null;
release_summary: string | null;
release_url: string | null;
skipped_version: string | null;
title: string | null;
update_percentage: number | null;
}
export interface UpdateEntity extends HassEntityBase {
attributes: UpdateEntityAttributes;
}
export const updateUsesProgress = (entity: UpdateEntity): boolean =>
supportsFeature(entity, UpdateEntityFeature.PROGRESS) &&
entity.attributes.update_percentage !== null;
export const updateAvailable = (
entity: UpdateEntity,
showSkipped = false
): boolean =>
entity.state === BINARY_STATE_ON ||
(showSkipped && Boolean(entity.attributes.skipped_version));
export const updateCanInstall = (
entity: UpdateEntity,
showSkipped = false
): boolean =>
updateAvailable(entity, showSkipped) &&
supportsFeature(entity, UpdateEntityFeature.INSTALL);
export const updateCanNotInstall = (
entity: UpdateEntity,
showSkipped = false
): boolean =>
updateAvailable(entity, showSkipped) &&
!supportsFeature(entity, UpdateEntityFeature.INSTALL);
export const latestVersionIsSkipped = (entity: UpdateEntity): boolean =>
!!(
entity.attributes.latest_version &&
entity.attributes.skipped_version === entity.attributes.latest_version
);
export const updateButtonIsDisabled = (entity: UpdateEntity): boolean =>
entity.state === BINARY_STATE_OFF && !latestVersionIsSkipped(entity);
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
!!entity.attributes.in_progress;
export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
hass.callWS<string | null>({
type: "update/release_notes",
entity_id: entityId,
});
const HOME_ASSISTANT_CORE_TITLE = "Home Assistant Core";
const HOME_ASSISTANT_SUPERVISOR_TITLE = "Home Assistant Supervisor";
const HOME_ASSISTANT_OS_TITLE = "Home Assistant Operating System";
export const filterUpdateEntities = (
entities: HassEntities,
language?: string
) =>
(
Object.values(entities).filter(
(entity) => computeStateDomain(entity) === "update"
) as UpdateEntity[]
).sort((a, b) => {
if (a.attributes.title === HOME_ASSISTANT_CORE_TITLE) {
return -3;
}
if (b.attributes.title === HOME_ASSISTANT_CORE_TITLE) {
return 3;
}
if (a.attributes.title === HOME_ASSISTANT_OS_TITLE) {
return -2;
}
if (b.attributes.title === HOME_ASSISTANT_OS_TITLE) {
return 2;
}
if (a.attributes.title === HOME_ASSISTANT_SUPERVISOR_TITLE) {
return -1;
}
if (b.attributes.title === HOME_ASSISTANT_SUPERVISOR_TITLE) {
return 1;
}
return caseInsensitiveStringCompare(
a.attributes.title || a.attributes.friendly_name || "",
b.attributes.title || b.attributes.friendly_name || "",
language
);
});
export const filterUpdateEntitiesParameterized = (
entities: HassEntities,
showSkipped = false,
showNotInstallable = false
) =>
filterUpdateEntities(entities).filter((entity) => {
if (showNotInstallable) {
return updateCanNotInstall(entity, showSkipped);
}
return updateCanInstall(entity, showSkipped);
});
export const checkForEntityUpdates = async (
element: HTMLElement,
hass: HomeAssistant
) => {
const entities = filterUpdateEntities(hass.states, hass.locale.language).map(
(entity) => entity.entity_id
);
if (!entities.length) {
showAlertDialog(element, {
title: hass.localize("ui.panel.config.updates.no_update_entities.title"),
text: hass.localize(
"ui.panel.config.updates.no_update_entities.description"
),
warning: true,
});
return;
}
showToast(element, {
message: hass.localize("ui.panel.config.updates.checking_updates"),
});
let updated = 0;
const unsubscribeEvents = await hass.connection.subscribeEvents<HassEvent>(
(event) => {
if (computeDomain(event.data.entity_id) === "update") {
updated++;
showToast(element, {
message: hass.localize("ui.panel.config.updates.updates_refreshed", {
count: updated,
}),
});
}
},
"state_changed"
);
await hass.callService("homeassistant", "update_entity", {
entity_id: entities,
});
// there is no reliable way to know if all the updates are done updating, so we just wait a bit for now...
await new Promise((r) => {
setTimeout(r, 15000);
});
unsubscribeEvents();
if (updated === 0) {
showToast(element, {
message: hass.localize("ui.panel.config.updates.no_new_updates"),
});
}
};
// When updating, and entity does not support % show "Installing"
// When updating, and entity does support % show "Installing (xx%)"
// When update available, show "Update available"
// When the latest version is skipped, show the latest version
// When update is not available, show "Up-to-date"
// When update is not available and there is no latest_version show "Unavailable"
export const computeUpdateStateDisplay = (
stateObj: UpdateEntity,
hass: HomeAssistant
): string => {
const state = stateObj.state;
const attributes = stateObj.attributes;
if (state === "off") {
const isSkipped =
attributes.latest_version &&
attributes.skipped_version === attributes.latest_version;
if (isSkipped) {
return attributes.latest_version!;
}
return hass.formatEntityState(stateObj);
}
if (state === "on") {
if (updateIsInstalling(stateObj)) {
const supportsProgress =
supportsFeature(stateObj, UpdateEntityFeature.PROGRESS) &&
attributes.update_percentage !== null;
if (supportsProgress) {
return hass.localize("ui.card.update.installing_with_progress", {
progress: formatNumber(attributes.update_percentage!, hass.locale, {
maximumFractionDigits: attributes.display_precision,
minimumFractionDigits: attributes.display_precision,
}),
});
}
return hass.localize("ui.card.update.installing");
}
}
return hass.formatEntityState(stateObj);
};
export type UpdateType =
| "addon"
| "home_assistant"
| "home_assistant_os"
| "generic";
export const getUpdateType = (
stateObj: UpdateEntity,
entitySources: EntitySources
): UpdateType => {
const entity_id = stateObj.entity_id;
const domain = entitySources[entity_id]?.domain;
if (domain !== "hassio") {
return "generic";
}
const title = stateObj.attributes.title || "";
if (title === HOME_ASSISTANT_CORE_TITLE) {
return "home_assistant";
}
if (title === HOME_ASSISTANT_OS_TITLE) {
return "home_assistant_os";
}
if (title !== HOME_ASSISTANT_SUPERVISOR_TITLE) {
return "addon";
}
return "generic";
};