Convert history calls to use new websocket endpoint (#12662)

This commit is contained in:
J. Nick Koston 2022-05-18 12:20:38 -05:00 committed by GitHub
parent 4cfb6713cb
commit f807618f75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 220 additions and 123 deletions

View File

@ -2,67 +2,74 @@ import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { FrontendLocaleData } from "../../data/translation";
import {
updateIsInstalling,
UpdateEntity,
UPDATE_SUPPORT_PROGRESS,
updateIsInstallingFromAttributes,
} from "../../data/update";
import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time";
import { formatNumber, isNumericState } from "../number/format_number";
import { formatNumber, isNumericFromAttributes } from "../number/format_number";
import { LocalizeFunc } from "../translations/localize";
import { computeStateDomain } from "./compute_state_domain";
import { supportsFeature } from "./supports-feature";
import { supportsFeatureFromAttributes } from "./supports-feature";
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
import { computeDomain } from "./compute_domain";
export const computeStateDisplay = (
localize: LocalizeFunc,
stateObj: HassEntity,
locale: FrontendLocaleData,
state?: string
): string => {
const compareState = state !== undefined ? state : stateObj.state;
): string =>
computeStateDisplayFromEntityAttributes(
localize,
locale,
stateObj.entity_id,
stateObj.attributes,
state !== undefined ? state : stateObj.state
);
if (compareState === UNKNOWN || compareState === UNAVAILABLE) {
return localize(`state.default.${compareState}`);
export const computeStateDisplayFromEntityAttributes = (
localize: LocalizeFunc,
locale: FrontendLocaleData,
entityId: string,
attributes: any,
state: string
): string => {
if (state === UNKNOWN || state === UNAVAILABLE) {
return localize(`state.default.${state}`);
}
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
if (isNumericState(stateObj)) {
if (isNumericFromAttributes(attributes)) {
// state is duration
if (
stateObj.attributes.device_class === "duration" &&
stateObj.attributes.unit_of_measurement &&
UNIT_TO_SECOND_CONVERT[stateObj.attributes.unit_of_measurement]
attributes.device_class === "duration" &&
attributes.unit_of_measurement &&
UNIT_TO_SECOND_CONVERT[attributes.unit_of_measurement]
) {
try {
return formatDuration(
compareState,
stateObj.attributes.unit_of_measurement
);
return formatDuration(state, attributes.unit_of_measurement);
} catch (_err) {
// fallback to default
}
}
if (stateObj.attributes.device_class === "monetary") {
if (attributes.device_class === "monetary") {
try {
return formatNumber(compareState, locale, {
return formatNumber(state, locale, {
style: "currency",
currency: stateObj.attributes.unit_of_measurement,
currency: attributes.unit_of_measurement,
minimumFractionDigits: 2,
});
} catch (_err) {
// fallback to default
}
}
return `${formatNumber(compareState, locale)}${
stateObj.attributes.unit_of_measurement
? " " + stateObj.attributes.unit_of_measurement
: ""
return `${formatNumber(state, locale)}${
attributes.unit_of_measurement ? " " + attributes.unit_of_measurement : ""
}`;
}
const domain = computeStateDomain(stateObj);
const domain = computeDomain(entityId);
if (domain === "input_datetime") {
if (state !== undefined) {
@ -97,36 +104,32 @@ export const computeStateDisplay = (
} else {
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
let date: Date;
if (stateObj.attributes.has_date && stateObj.attributes.has_time) {
if (attributes.has_date && attributes.has_time) {
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day,
stateObj.attributes.hour,
stateObj.attributes.minute
attributes.year,
attributes.month - 1,
attributes.day,
attributes.hour,
attributes.minute
);
return formatDateTime(date, locale);
}
if (stateObj.attributes.has_date) {
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day
);
if (attributes.has_date) {
date = new Date(attributes.year, attributes.month - 1, attributes.day);
return formatDate(date, locale);
}
if (stateObj.attributes.has_time) {
if (attributes.has_time) {
date = new Date();
date.setHours(stateObj.attributes.hour, stateObj.attributes.minute);
date.setHours(attributes.hour, attributes.minute);
return formatTime(date, locale);
}
return stateObj.state;
return state;
}
}
if (domain === "humidifier") {
if (compareState === "on" && stateObj.attributes.humidity) {
return `${stateObj.attributes.humidity} %`;
if (state === "on" && attributes.humidity) {
return `${attributes.humidity} %`;
}
}
@ -136,7 +139,7 @@ export const computeStateDisplay = (
domain === "number" ||
domain === "input_number"
) {
return formatNumber(compareState, locale);
return formatNumber(state, locale);
}
// state of button is a timestamp
@ -144,12 +147,12 @@ export const computeStateDisplay = (
domain === "button" ||
domain === "input_button" ||
domain === "scene" ||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp")
(domain === "sensor" && attributes.device_class === "timestamp")
) {
try {
return formatDateTime(new Date(compareState), locale);
return formatDateTime(new Date(state), locale);
} catch (_err) {
return compareState;
return state;
}
}
@ -160,30 +163,28 @@ export const computeStateDisplay = (
// 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"
return compareState === "on"
? updateIsInstalling(stateObj as UpdateEntity)
? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS)
return state === "on"
? updateIsInstallingFromAttributes(attributes)
? supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS)
? localize("ui.card.update.installing_with_progress", {
progress: stateObj.attributes.in_progress,
progress: attributes.in_progress,
})
: localize("ui.card.update.installing")
: stateObj.attributes.latest_version
: stateObj.attributes.skipped_version ===
stateObj.attributes.latest_version
? stateObj.attributes.latest_version ??
localize("state.default.unavailable")
: attributes.latest_version
: attributes.skipped_version === attributes.latest_version
? attributes.latest_version ?? localize("state.default.unavailable")
: localize("ui.card.update.up_to_date");
}
return (
// Return device class translation
(stateObj.attributes.device_class &&
(attributes.device_class &&
localize(
`component.${domain}.state.${stateObj.attributes.device_class}.${compareState}`
`component.${domain}.state.${attributes.device_class}.${state}`
)) ||
// Return default translation
localize(`component.${domain}.state._.${compareState}`) ||
localize(`component.${domain}.state._.${state}`) ||
// We don't know! Return the raw state.
compareState
state
);
};

View File

@ -1,7 +1,13 @@
import { HassEntity } from "home-assistant-js-websocket";
import { computeObjectId } from "./compute_object_id";
export const computeStateNameFromEntityAttributes = (
entityId: string,
attributes: { [key: string]: any }
): string =>
attributes.friendly_name === undefined
? computeObjectId(entityId).replace(/_/g, " ")
: attributes.friendly_name || "";
export const computeStateName = (stateObj: HassEntity): string =>
stateObj.attributes.friendly_name === undefined
? computeObjectId(stateObj.entity_id).replace(/_/g, " ")
: stateObj.attributes.friendly_name || "";
computeStateNameFromEntityAttributes(stateObj.entity_id, stateObj.attributes);

View File

@ -3,6 +3,13 @@ import { HassEntity } from "home-assistant-js-websocket";
export const supportsFeature = (
stateObj: HassEntity,
feature: number
): boolean => supportsFeatureFromAttributes(stateObj.attributes, feature);
export const supportsFeatureFromAttributes = (
attributes: {
[key: string]: any;
},
feature: number
): boolean =>
// eslint-disable-next-line no-bitwise
(stateObj.attributes.supported_features! & feature) !== 0;
(attributes.supported_features! & feature) !== 0;

View File

@ -7,8 +7,11 @@ import { round } from "./round";
* @param stateObj The entity state object
*/
export const isNumericState = (stateObj: HassEntity): boolean =>
!!stateObj.attributes.unit_of_measurement ||
!!stateObj.attributes.state_class;
isNumericFromAttributes(stateObj.attributes);
export const isNumericFromAttributes = (attributes: {
[key: string]: any;
}): boolean => !!attributes.unit_of_measurement || !!attributes.state_class;
export const numberFormatToLocale = (
localeOptions: FrontendLocaleData

View File

@ -1,13 +1,13 @@
import { HassEntity } from "home-assistant-js-websocket";
import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types";
import {
computeHistory,
fetchRecent,
HistoryStates,
HistoryResult,
LineChartUnit,
TimelineEntity,
entityIdHistoryNeedsAttributes,
fetchRecentWS,
} from "./history";
export interface CacheConfig {
@ -55,7 +55,7 @@ export const getRecent = (
}
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
const prom = fetchRecent(
const prom = fetchRecentWS(
hass,
entityId,
startTime,
@ -134,12 +134,12 @@ export const getRecentWithCache = (
const noAttributes = !entityIdHistoryNeedsAttributes(hass, entityId);
const genProm = async () => {
let fetchedHistory: HassEntity[][];
let fetchedHistory: HistoryStates;
try {
const results = await Promise.all([
curCacheProm,
fetchRecent(
fetchRecentWS(
hass,
entityId,
toFetchStartTime,

View File

@ -1,8 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display";
import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name";
import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types";
import { FrontendLocaleData } from "./translation";
@ -27,7 +26,7 @@ const LINE_ATTRIBUTES_TO_KEEP = [
export interface LineChartState {
state: string;
last_changed: string;
last_changed: number;
attributes?: Record<string, any>;
}
@ -47,7 +46,7 @@ export interface LineChartUnit {
export interface TimelineState {
state_localize: string;
state: string;
last_changed: string;
last_changed: number;
}
export interface TimelineEntity {
@ -141,6 +140,21 @@ export interface StatisticsValidationResults {
[statisticId: string]: StatisticsValidationResult[];
}
export interface HistoryStates {
[entityId: string]: EntityHistoryState[];
}
interface EntityHistoryState {
/** state */
s: string;
/** attributes */
a: { [key: string]: any };
/** last_changed; if set, also applies to lu */
lc: number;
/** last_updated */
lu: number;
}
export const entityIdHistoryNeedsAttributes = (
hass: HomeAssistant,
entityId: string
@ -181,6 +195,27 @@ export const fetchRecent = (
return hass.callApi("GET", url);
};
export const fetchRecentWS = (
hass: HomeAssistant,
entityId: string,
startTime: Date,
endTime: Date,
skipInitialState = false,
significantChangesOnly?: boolean,
minimalResponse = true,
noAttributes?: boolean
) =>
hass.callWS<HistoryStates>({
type: "history/history_during_period",
start_time: startTime.toISOString(),
end_time: endTime.toISOString(),
significant_changes_only: significantChangesOnly || false,
include_start_time_state: !skipInitialState,
minimal_response: minimalResponse,
no_attributes: noAttributes || false,
entity_ids: [entityId],
});
export const fetchDate = (
hass: HomeAssistant,
startTime: Date,
@ -198,6 +233,27 @@ export const fetchDate = (
}`
);
export const fetchDateWS = (
hass: HomeAssistant,
startTime: Date,
endTime: Date,
entityId?: string
) => {
const params = {
type: "history/history_during_period",
start_time: startTime.toISOString(),
end_time: endTime.toISOString(),
minimal_response: true,
no_attributes: !!(
entityId && !entityIdHistoryNeedsAttributes(hass, entityId)
),
};
if (entityId) {
return hass.callWS<HistoryStates>({ ...params, entity_ids: [entityId] });
}
return hass.callWS<HistoryStates>(params);
};
const equalState = (obj1: LineChartState, obj2: LineChartState) =>
obj1.state === obj2.state &&
// Only compare attributes if both states have an attributes object.
@ -212,46 +268,47 @@ const equalState = (obj1: LineChartState, obj2: LineChartState) =>
const processTimelineEntity = (
localize: LocalizeFunc,
language: FrontendLocaleData,
states: HassEntity[]
entityId: string,
states: EntityHistoryState[]
): TimelineEntity => {
const data: TimelineState[] = [];
const last_element = states.length - 1;
const last: EntityHistoryState = states[states.length - 1];
for (const state of states) {
if (data.length > 0 && state.state === data[data.length - 1].state) {
if (data.length > 0 && state.s === data[data.length - 1].state) {
continue;
}
// Copy the data from the last element as its the newest
// and is only needed to localize the data
if (!state.entity_id) {
state.attributes = states[last_element].attributes;
state.entity_id = states[last_element].entity_id;
}
data.push({
state_localize: computeStateDisplay(localize, state, language),
state: state.state,
last_changed: state.last_changed,
state_localize: computeStateDisplayFromEntityAttributes(
localize,
language,
entityId,
state.a || last.a,
state.s
),
state: state.s,
// lc (last_changed) may be omitted if its the same
// as lu (last_updated).
last_changed: (state.lc ? state.lc : state.lu) * 1000,
});
}
return {
name: computeStateName(states[0]),
entity_id: states[0].entity_id,
name: computeStateNameFromEntityAttributes(entityId, states[0].a),
entity_id: entityId,
data,
};
};
const processLineChartEntities = (
unit,
entities: HassEntity[][]
entities: HistoryStates
): LineChartUnit => {
const data: LineChartEntity[] = [];
for (const states of entities) {
const last: HassEntity = states[states.length - 1];
const domain = computeStateDomain(last);
Object.keys(entities).forEach((entityId) => {
const states = entities[entityId];
const last: EntityHistoryState = states[states.length - 1];
const domain = computeDomain(entityId);
const processedStates: LineChartState[] = [];
for (const state of states) {
@ -259,18 +316,24 @@ const processLineChartEntities = (
if (DOMAINS_USE_LAST_UPDATED.includes(domain)) {
processedState = {
state: state.state,
last_changed: state.last_updated,
state: state.s,
last_changed: state.lu * 1000,
attributes: {},
};
for (const attr of LINE_ATTRIBUTES_TO_KEEP) {
if (attr in state.attributes) {
processedState.attributes![attr] = state.attributes[attr];
if (attr in state.a) {
processedState.attributes![attr] = state.a[attr];
}
}
} else {
processedState = state;
processedState = {
state: state.s,
// lc (last_changed) may be omitted if its the same
// as lu (last_updated).
last_changed: (state.lc ? state.lc : state.lu) * 1000,
attributes: {},
};
}
if (
@ -289,52 +352,53 @@ const processLineChartEntities = (
data.push({
domain,
name: computeStateName(last),
entity_id: last.entity_id,
name: computeStateNameFromEntityAttributes(entityId, last.a),
entity_id: entityId,
states: processedStates,
});
}
});
return {
unit,
identifier: entities.map((states) => states[0].entity_id).join(""),
identifier: Object.keys(entities).join(""),
data,
};
};
const stateUsesUnits = (state: HassEntity) =>
"unit_of_measurement" in state.attributes ||
"state_class" in state.attributes;
attributesHaveUnits(state.attributes);
const attributesHaveUnits = (attributes: { [key: string]: any }) =>
"unit_of_measurement" in attributes || "state_class" in attributes;
export const computeHistory = (
hass: HomeAssistant,
stateHistory: HassEntity[][],
stateHistory: HistoryStates,
localize: LocalizeFunc
): HistoryResult => {
const lineChartDevices: { [unit: string]: HassEntity[][] } = {};
const lineChartDevices: { [unit: string]: HistoryStates } = {};
const timelineDevices: TimelineEntity[] = [];
if (!stateHistory) {
return { line: [], timeline: [] };
}
stateHistory.forEach((stateInfo) => {
Object.keys(stateHistory).forEach((entityId) => {
const stateInfo = stateHistory[entityId];
if (stateInfo.length === 0) {
return;
}
const entityId = stateInfo[0].entity_id;
const currentState =
entityId in hass.states ? hass.states[entityId] : undefined;
const stateWithUnitorStateClass =
!currentState &&
stateInfo.find((state) => state.attributes && stateUsesUnits(state));
stateInfo.find((state) => state.a && attributesHaveUnits(state.a));
let unit: string | undefined;
if (currentState && stateUsesUnits(currentState)) {
unit = currentState.attributes.unit_of_measurement || " ";
} else if (stateWithUnitorStateClass) {
unit = stateWithUnitorStateClass.attributes.unit_of_measurement || " ";
unit = stateWithUnitorStateClass.a.unit_of_measurement || " ";
} else {
unit = {
climate: hass.config.unit_system.temperature,
@ -348,12 +412,15 @@ export const computeHistory = (
if (!unit) {
timelineDevices.push(
processTimelineEntity(localize, hass.locale, stateInfo)
processTimelineEntity(localize, hass.locale, entityId, stateInfo)
);
} else if (unit in lineChartDevices) {
lineChartDevices[unit].push(stateInfo);
} else if (unit in lineChartDevices && entityId in lineChartDevices[unit]) {
lineChartDevices[unit][entityId].push(...stateInfo);
} else {
lineChartDevices[unit] = [stateInfo];
if (!(unit in lineChartDevices)) {
lineChartDevices[unit] = {};
}
lineChartDevices[unit][entityId] = stateInfo;
}
});

View File

@ -7,7 +7,10 @@ import type {
import { 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 {
supportsFeature,
supportsFeatureFromAttributes,
} from "../common/entity/supports-feature";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../types";
@ -35,8 +38,13 @@ export interface UpdateEntity extends HassEntityBase {
}
export const updateUsesProgress = (entity: UpdateEntity): boolean =>
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
typeof entity.attributes.in_progress === "number";
updateUsesProgressFromAttributes(entity.attributes);
export const updateUsesProgressFromAttributes = (attributes: {
[key: string]: any;
}): boolean =>
supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) &&
typeof attributes.in_progress === "number";
export const updateCanInstall = (
entity: UpdateEntity,
@ -49,6 +57,11 @@ export const updateCanInstall = (
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
updateUsesProgress(entity) || !!entity.attributes.in_progress;
export const updateIsInstallingFromAttributes = (attributes: {
[key: string]: any;
}): boolean =>
updateUsesProgressFromAttributes(attributes) || !!attributes.in_progress;
export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
hass.callWS<string | null>({
type: "update/release_notes",

View File

@ -25,7 +25,7 @@ import "../../components/ha-date-range-picker";
import type { DateRangePickerRanges } from "../../components/ha-date-range-picker";
import "../../components/ha-icon-button";
import "../../components/ha-menu-button";
import { computeHistory, fetchDate } from "../../data/history";
import { computeHistory, fetchDateWS } from "../../data/history";
import "../../layouts/ha-app-layout";
import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types";
@ -177,7 +177,7 @@ class HaPanelHistory extends LitElement {
private async _getHistory() {
this._isLoading = true;
const dateHistory = await fetchDate(
const dateHistory = await fetchDateWS(
this.hass,
this._startDate,
this._endDate,