mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-21 08:16:36 +00:00
TS history data (#1839)
* Convert history data to TS * Lint * Extract cached history * Move around
This commit is contained in:
parent
ad162677a6
commit
d0cb7b9724
@ -3,7 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag.js";
|
|||||||
import { PolymerElement } from "@polymer/polymer/polymer-element.js";
|
import { PolymerElement } from "@polymer/polymer/polymer-element.js";
|
||||||
|
|
||||||
import "../components/state-history-charts.js";
|
import "../components/state-history-charts.js";
|
||||||
import "../data/ha-state-history-data.js";
|
import "../data/ha-state-history-data";
|
||||||
|
|
||||||
import computeStateName from "../common/entity/compute_state_name.js";
|
import computeStateName from "../common/entity/compute_state_name.js";
|
||||||
import EventsMixin from "../mixins/events-mixin.js";
|
import EventsMixin from "../mixins/events-mixin.js";
|
||||||
|
@ -255,7 +255,7 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
|
|||||||
Array.prototype.push.apply(datasets, data);
|
Array.prototype.push.apply(datasets, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatTooltipTitle = function(items, data) {
|
const formatTooltipTitle = (items, data) => {
|
||||||
const item = items[0];
|
const item = items[0];
|
||||||
const date = data.datasets[item.datasetIndex].data[item.index].x;
|
const date = data.datasets[item.datasetIndex].data[item.index].x;
|
||||||
|
|
||||||
|
235
src/data/cached-history.ts
Normal file
235
src/data/cached-history.ts
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
import {
|
||||||
|
computeHistory,
|
||||||
|
fetchRecent,
|
||||||
|
HistoryResult,
|
||||||
|
TimelineEntity,
|
||||||
|
LineChartUnit,
|
||||||
|
} from "./history";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
import { LocalizeFunc } from "../mixins/localize-base-mixin";
|
||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
|
||||||
|
interface CacheConfig {
|
||||||
|
refresh: number;
|
||||||
|
cacheKey: string;
|
||||||
|
hoursToShow: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CachedResults {
|
||||||
|
prom: Promise<HistoryResult>;
|
||||||
|
startTime: Date;
|
||||||
|
endTime: Date;
|
||||||
|
language: string;
|
||||||
|
data: HistoryResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a different interface, a different cache :(
|
||||||
|
interface RecentCacheResults {
|
||||||
|
created: number;
|
||||||
|
language: string;
|
||||||
|
data: Promise<HistoryResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RECENT_THRESHOLD = 60000; // 1 minute
|
||||||
|
const RECENT_CACHE: { [cacheKey: string]: RecentCacheResults } = {};
|
||||||
|
const stateHistoryCache: { [cacheKey: string]: CachedResults } = {};
|
||||||
|
|
||||||
|
// Cached type 1 unction. Without cache config.
|
||||||
|
export const getRecent = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entityId: string,
|
||||||
|
startTime: Date,
|
||||||
|
endTime: Date,
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
language: string
|
||||||
|
) => {
|
||||||
|
const cacheKey = entityId;
|
||||||
|
const cache = RECENT_CACHE[cacheKey];
|
||||||
|
|
||||||
|
if (
|
||||||
|
cache &&
|
||||||
|
Date.now() - cache.created < RECENT_THRESHOLD &&
|
||||||
|
cache.language === language
|
||||||
|
) {
|
||||||
|
return cache.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prom = fetchRecent(hass, entityId, startTime, endTime).then(
|
||||||
|
(stateHistory) => computeHistory(hass, stateHistory, localize, language),
|
||||||
|
(err) => {
|
||||||
|
delete RECENT_CACHE[entityId];
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
RECENT_CACHE[cacheKey] = {
|
||||||
|
created: Date.now(),
|
||||||
|
language,
|
||||||
|
data: prom,
|
||||||
|
};
|
||||||
|
return prom;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache type 2 functionality
|
||||||
|
function getEmptyCache(
|
||||||
|
language: string,
|
||||||
|
startTime: Date,
|
||||||
|
endTime: Date
|
||||||
|
): CachedResults {
|
||||||
|
return {
|
||||||
|
prom: Promise.resolve({ line: [], timeline: [] }),
|
||||||
|
language,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
data: { line: [], timeline: [] },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRecentWithCache = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entityId: string,
|
||||||
|
cacheConfig: CacheConfig,
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
language: string
|
||||||
|
) => {
|
||||||
|
const cacheKey = cacheConfig.cacheKey;
|
||||||
|
const endTime = new Date();
|
||||||
|
const startTime = new Date(endTime);
|
||||||
|
startTime.setHours(startTime.getHours() - cacheConfig.hoursToShow);
|
||||||
|
let toFetchStartTime = startTime;
|
||||||
|
let appendingToCache = false;
|
||||||
|
|
||||||
|
let cache = stateHistoryCache[cacheKey];
|
||||||
|
if (
|
||||||
|
cache &&
|
||||||
|
toFetchStartTime >= cache.startTime &&
|
||||||
|
toFetchStartTime <= cache.endTime &&
|
||||||
|
cache.language === language
|
||||||
|
) {
|
||||||
|
toFetchStartTime = cache.endTime;
|
||||||
|
appendingToCache = true;
|
||||||
|
// This pretty much never happens as endTime is usually set to now
|
||||||
|
if (endTime <= cache.endTime) {
|
||||||
|
return cache.prom;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cache = stateHistoryCache[cacheKey] = getEmptyCache(
|
||||||
|
language,
|
||||||
|
startTime,
|
||||||
|
endTime
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const curCacheProm = cache.prom;
|
||||||
|
|
||||||
|
const genProm = async () => {
|
||||||
|
let fetchedHistory: HassEntity[][];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await Promise.all([
|
||||||
|
curCacheProm,
|
||||||
|
fetchRecent(
|
||||||
|
hass,
|
||||||
|
entityId,
|
||||||
|
toFetchStartTime,
|
||||||
|
endTime,
|
||||||
|
appendingToCache
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
fetchedHistory = results[1];
|
||||||
|
} catch (err) {
|
||||||
|
delete stateHistoryCache[cacheKey];
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const stateHistory = computeHistory(
|
||||||
|
hass,
|
||||||
|
fetchedHistory,
|
||||||
|
localize,
|
||||||
|
language
|
||||||
|
);
|
||||||
|
if (appendingToCache) {
|
||||||
|
mergeLine(stateHistory.line, cache.data.line);
|
||||||
|
mergeTimeline(stateHistory.timeline, cache.data.timeline);
|
||||||
|
pruneStartTime(startTime, cache.data);
|
||||||
|
} else {
|
||||||
|
cache.data = stateHistory;
|
||||||
|
}
|
||||||
|
return cache.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
cache.prom = genProm();
|
||||||
|
cache.startTime = startTime;
|
||||||
|
cache.endTime = endTime;
|
||||||
|
return cache.prom;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergeLine = (
|
||||||
|
historyLines: LineChartUnit[],
|
||||||
|
cacheLines: LineChartUnit[]
|
||||||
|
) => {
|
||||||
|
historyLines.forEach((line) => {
|
||||||
|
const unit = line.unit;
|
||||||
|
const oldLine = cacheLines.find((cacheLine) => cacheLine.unit === unit);
|
||||||
|
if (oldLine) {
|
||||||
|
line.data.forEach((entity) => {
|
||||||
|
const oldEntity = oldLine.data.find(
|
||||||
|
(cacheEntity) => entity.entity_id === cacheEntity.entity_id
|
||||||
|
);
|
||||||
|
if (oldEntity) {
|
||||||
|
oldEntity.states = oldEntity.states.concat(entity.states);
|
||||||
|
} else {
|
||||||
|
oldLine.data.push(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cacheLines.push(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergeTimeline = (
|
||||||
|
historyTimelines: TimelineEntity[],
|
||||||
|
cacheTimelines: TimelineEntity[]
|
||||||
|
) => {
|
||||||
|
historyTimelines.forEach((timeline) => {
|
||||||
|
const oldTimeline = cacheTimelines.find(
|
||||||
|
(cacheTimeline) => cacheTimeline.entity_id === timeline.entity_id
|
||||||
|
);
|
||||||
|
if (oldTimeline) {
|
||||||
|
oldTimeline.data = oldTimeline.data.concat(timeline.data);
|
||||||
|
} else {
|
||||||
|
cacheTimelines.push(timeline);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const pruneArray = (originalStartTime: Date, arr) => {
|
||||||
|
if (arr.length === 0) {
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
const changedAfterStartTime = arr.findIndex(
|
||||||
|
(state) => new Date(state.last_changed) > originalStartTime
|
||||||
|
);
|
||||||
|
if (changedAfterStartTime === 0) {
|
||||||
|
// If all changes happened after originalStartTime then we are done.
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all changes happened at or before originalStartTime. Use last index.
|
||||||
|
const updateIndex =
|
||||||
|
changedAfterStartTime === -1 ? arr.length - 1 : changedAfterStartTime - 1;
|
||||||
|
arr[updateIndex].last_changed = originalStartTime;
|
||||||
|
return arr.slice(updateIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pruneStartTime = (originalStartTime: Date, cacheData: HistoryResult) => {
|
||||||
|
cacheData.line.forEach((line) => {
|
||||||
|
line.data.forEach((entity) => {
|
||||||
|
entity.states = pruneArray(originalStartTime, entity.states);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cacheData.timeline.forEach((timeline) => {
|
||||||
|
timeline.data = pruneArray(originalStartTime, timeline.data);
|
||||||
|
});
|
||||||
|
};
|
@ -2,120 +2,10 @@ import { timeOut } from "@polymer/polymer/lib/utils/async.js";
|
|||||||
import { Debouncer } from "@polymer/polymer/lib/utils/debounce.js";
|
import { Debouncer } from "@polymer/polymer/lib/utils/debounce.js";
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element.js";
|
import { PolymerElement } from "@polymer/polymer/polymer-element.js";
|
||||||
|
|
||||||
import computeStateName from "../common/entity/compute_state_name.js";
|
|
||||||
import computeStateDomain from "../common/entity/compute_state_domain.js";
|
|
||||||
import computeStateDisplay from "../common/entity/compute_state_display.js";
|
|
||||||
import LocalizeMixin from "../mixins/localize-mixin.js";
|
import LocalizeMixin from "../mixins/localize-mixin.js";
|
||||||
|
|
||||||
const RECENT_THRESHOLD = 60000; // 1 minute
|
import { computeHistory, fetchDate } from "./history";
|
||||||
const RECENT_CACHE = {};
|
import { getRecent, getRecentWithCache } from "./cached-history";
|
||||||
const DOMAINS_USE_LAST_UPDATED = ["thermostat", "climate", "water_heater"];
|
|
||||||
const LINE_ATTRIBUTES_TO_KEEP = [
|
|
||||||
"temperature",
|
|
||||||
"current_temperature",
|
|
||||||
"target_temp_low",
|
|
||||||
"target_temp_high",
|
|
||||||
];
|
|
||||||
const stateHistoryCache = {};
|
|
||||||
|
|
||||||
function computeHistory(hass, stateHistory, localize, language) {
|
|
||||||
const lineChartDevices = {};
|
|
||||||
const timelineDevices = [];
|
|
||||||
if (!stateHistory) {
|
|
||||||
return { line: [], timeline: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
stateHistory.forEach((stateInfo) => {
|
|
||||||
if (stateInfo.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateWithUnit = stateInfo.find(
|
|
||||||
(state) => "unit_of_measurement" in state.attributes
|
|
||||||
);
|
|
||||||
|
|
||||||
let unit = false;
|
|
||||||
if (stateWithUnit) {
|
|
||||||
unit = stateWithUnit.attributes.unit_of_measurement;
|
|
||||||
} else if (computeStateDomain(stateInfo[0]) === "climate") {
|
|
||||||
unit = hass.config.unit_system.temperature;
|
|
||||||
} else if (computeStateDomain(stateInfo[0]) === "water_heater") {
|
|
||||||
unit = hass.config.unit_system.temperature;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!unit) {
|
|
||||||
timelineDevices.push({
|
|
||||||
name: computeStateName(stateInfo[0]),
|
|
||||||
entity_id: stateInfo[0].entity_id,
|
|
||||||
data: stateInfo
|
|
||||||
.map((state) => ({
|
|
||||||
state_localize: computeStateDisplay(localize, state, language),
|
|
||||||
state: state.state,
|
|
||||||
last_changed: state.last_changed,
|
|
||||||
}))
|
|
||||||
.filter((element, index, arr) => {
|
|
||||||
if (index === 0) return true;
|
|
||||||
return element.state !== arr[index - 1].state;
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} else if (unit in lineChartDevices) {
|
|
||||||
lineChartDevices[unit].push(stateInfo);
|
|
||||||
} else {
|
|
||||||
lineChartDevices[unit] = [stateInfo];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const unitStates = Object.keys(lineChartDevices).map((unit) => ({
|
|
||||||
unit: unit,
|
|
||||||
identifier: lineChartDevices[unit]
|
|
||||||
.map((states) => states[0].entity_id)
|
|
||||||
.join(""),
|
|
||||||
data: lineChartDevices[unit].map((states) => {
|
|
||||||
const last = states[states.length - 1];
|
|
||||||
const domain = computeStateDomain(last);
|
|
||||||
return {
|
|
||||||
domain: domain,
|
|
||||||
name: computeStateName(last),
|
|
||||||
entity_id: last.entity_id,
|
|
||||||
states: states
|
|
||||||
.map((state) => {
|
|
||||||
const result = {
|
|
||||||
state: state.state,
|
|
||||||
last_changed: state.last_changed,
|
|
||||||
};
|
|
||||||
if (DOMAINS_USE_LAST_UPDATED.includes(domain)) {
|
|
||||||
result.last_changed = state.last_updated;
|
|
||||||
}
|
|
||||||
LINE_ATTRIBUTES_TO_KEEP.forEach((attr) => {
|
|
||||||
if (attr in state.attributes) {
|
|
||||||
result.attributes = result.attributes || {};
|
|
||||||
result.attributes[attr] = state.attributes[attr];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
})
|
|
||||||
.filter((element, index, arr) => {
|
|
||||||
// Remove data point if it is equal to previous point and next point.
|
|
||||||
if (index === 0 || index === arr.length - 1) return true;
|
|
||||||
function compare(obj1, obj2) {
|
|
||||||
if (obj1.state !== obj2.state) return false;
|
|
||||||
if (!obj1.attributes && !obj2.attributes) return true;
|
|
||||||
if (!obj1.attributes || !obj2.attributes) return false;
|
|
||||||
return LINE_ATTRIBUTES_TO_KEEP.every(
|
|
||||||
(attr) => obj1.attributes[attr] === obj2.attributes[attr]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
!compare(element, arr[index - 1]) ||
|
|
||||||
!compare(element, arr[index + 1])
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return { line: unitStates, timeline: timelineDevices };
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @appliesMixin LocalizeMixin
|
* @appliesMixin LocalizeMixin
|
||||||
@ -225,7 +115,10 @@ class HaStateHistoryData extends LocalizeMixin(PolymerElement) {
|
|||||||
|
|
||||||
if (filterType === "date") {
|
if (filterType === "date") {
|
||||||
if (!startTime || !endTime) return;
|
if (!startTime || !endTime) return;
|
||||||
data = this.getDate(startTime, endTime, localize, language);
|
|
||||||
|
data = fetchDate(this.hass, startTime, endTime).then((dateHistory) =>
|
||||||
|
computeHistory(this.hass, dateHistory, localize, language)
|
||||||
|
);
|
||||||
} else if (filterType === "recent-entity") {
|
} else if (filterType === "recent-entity") {
|
||||||
if (!entityId) return;
|
if (!entityId) return;
|
||||||
if (cacheConfig) {
|
if (cacheConfig) {
|
||||||
@ -236,7 +129,14 @@ class HaStateHistoryData extends LocalizeMixin(PolymerElement) {
|
|||||||
language
|
language
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
data = this.getRecent(entityId, startTime, endTime, localize, language);
|
data = getRecent(
|
||||||
|
this.hass,
|
||||||
|
entityId,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
localize,
|
||||||
|
language
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
@ -249,14 +149,6 @@ class HaStateHistoryData extends LocalizeMixin(PolymerElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getEmptyCache(language) {
|
|
||||||
return {
|
|
||||||
prom: Promise.resolve({ line: [], timeline: [] }),
|
|
||||||
language: language,
|
|
||||||
data: { line: [], timeline: [] },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getRecentWithCacheRefresh(entityId, cacheConfig, localize, language) {
|
getRecentWithCacheRefresh(entityId, cacheConfig, localize, language) {
|
||||||
if (this._refreshTimeoutId) {
|
if (this._refreshTimeoutId) {
|
||||||
window.clearInterval(this._refreshTimeoutId);
|
window.clearInterval(this._refreshTimeoutId);
|
||||||
@ -264,193 +156,24 @@ class HaStateHistoryData extends LocalizeMixin(PolymerElement) {
|
|||||||
}
|
}
|
||||||
if (cacheConfig.refresh) {
|
if (cacheConfig.refresh) {
|
||||||
this._refreshTimeoutId = window.setInterval(() => {
|
this._refreshTimeoutId = window.setInterval(() => {
|
||||||
this.getRecentWithCache(entityId, cacheConfig, localize, language).then(
|
getRecentWithCache(
|
||||||
(stateHistory) => {
|
this.hass,
|
||||||
|
entityId,
|
||||||
|
cacheConfig,
|
||||||
|
localize,
|
||||||
|
language
|
||||||
|
).then((stateHistory) => {
|
||||||
this._setData(Object.assign({}, stateHistory));
|
this._setData(Object.assign({}, stateHistory));
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}, cacheConfig.refresh * 1000);
|
}, cacheConfig.refresh * 1000);
|
||||||
}
|
}
|
||||||
return this.getRecentWithCache(entityId, cacheConfig, localize, language);
|
return getRecentWithCache(
|
||||||
}
|
this.hass,
|
||||||
|
entityId,
|
||||||
mergeLine(historyLines, cacheLines) {
|
cacheConfig,
|
||||||
historyLines.forEach((line) => {
|
localize,
|
||||||
const unit = line.unit;
|
language
|
||||||
const oldLine = cacheLines.find((cacheLine) => cacheLine.unit === unit);
|
|
||||||
if (oldLine) {
|
|
||||||
line.data.forEach((entity) => {
|
|
||||||
const oldEntity = oldLine.data.find(
|
|
||||||
(cacheEntity) => entity.entity_id === cacheEntity.entity_id
|
|
||||||
);
|
);
|
||||||
if (oldEntity) {
|
|
||||||
oldEntity.states = oldEntity.states.concat(entity.states);
|
|
||||||
} else {
|
|
||||||
oldLine.data.push(entity);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
cacheLines.push(line);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeTimeline(historyTimelines, cacheTimelines) {
|
|
||||||
historyTimelines.forEach((timeline) => {
|
|
||||||
const oldTimeline = cacheTimelines.find(
|
|
||||||
(cacheTimeline) => cacheTimeline.entity_id === timeline.entity_id
|
|
||||||
);
|
|
||||||
if (oldTimeline) {
|
|
||||||
oldTimeline.data = oldTimeline.data.concat(timeline.data);
|
|
||||||
} else {
|
|
||||||
cacheTimelines.push(timeline);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pruneArray(originalStartTime, arr) {
|
|
||||||
if (arr.length === 0) return arr;
|
|
||||||
const changedAfterStartTime = arr.findIndex((state) => {
|
|
||||||
const lastChanged = new Date(state.last_changed);
|
|
||||||
return lastChanged > originalStartTime;
|
|
||||||
});
|
|
||||||
if (changedAfterStartTime === 0) {
|
|
||||||
// If all changes happened after originalStartTime then we are done.
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If all changes happened at or before originalStartTime. Use last index.
|
|
||||||
const updateIndex =
|
|
||||||
changedAfterStartTime === -1 ? arr.length - 1 : changedAfterStartTime - 1;
|
|
||||||
arr[updateIndex].last_changed = originalStartTime;
|
|
||||||
return arr.slice(updateIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
pruneStartTime(originalStartTime, cacheData) {
|
|
||||||
cacheData.line.forEach((line) => {
|
|
||||||
line.data.forEach((entity) => {
|
|
||||||
entity.states = this.pruneArray(originalStartTime, entity.states);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cacheData.timeline.forEach((timeline) => {
|
|
||||||
timeline.data = this.pruneArray(originalStartTime, timeline.data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getRecentWithCache(entityId, cacheConfig, localize, language) {
|
|
||||||
const cacheKey = cacheConfig.cacheKey;
|
|
||||||
const endTime = new Date();
|
|
||||||
const originalStartTime = new Date(endTime);
|
|
||||||
originalStartTime.setHours(
|
|
||||||
originalStartTime.getHours() - cacheConfig.hoursToShow
|
|
||||||
);
|
|
||||||
let startTime = originalStartTime;
|
|
||||||
let appendingToCache = false;
|
|
||||||
let cache = stateHistoryCache[cacheKey];
|
|
||||||
if (
|
|
||||||
cache &&
|
|
||||||
startTime >= cache.startTime &&
|
|
||||||
startTime <= cache.endTime &&
|
|
||||||
cache.language === language
|
|
||||||
) {
|
|
||||||
startTime = cache.endTime;
|
|
||||||
appendingToCache = true;
|
|
||||||
if (endTime <= cache.endTime) {
|
|
||||||
return cache.prom;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cache = stateHistoryCache[cacheKey] = this.getEmptyCache(language);
|
|
||||||
}
|
|
||||||
// Use Promise.all in order to make sure the old and the new fetches have both completed.
|
|
||||||
const prom = Promise.all([
|
|
||||||
cache.prom,
|
|
||||||
this.fetchRecent(entityId, startTime, endTime, appendingToCache),
|
|
||||||
])
|
|
||||||
// Use only data from the new fetch. Old fetch is already stored in cache.data
|
|
||||||
.then((oldAndNew) => oldAndNew[1])
|
|
||||||
// Convert data into format state-history-chart-* understands.
|
|
||||||
.then((stateHistory) =>
|
|
||||||
computeHistory(this.hass, stateHistory, localize, language)
|
|
||||||
)
|
|
||||||
// Merge old and new.
|
|
||||||
.then((stateHistory) => {
|
|
||||||
this.mergeLine(stateHistory.line, cache.data.line);
|
|
||||||
this.mergeTimeline(stateHistory.timeline, cache.data.timeline);
|
|
||||||
if (appendingToCache) {
|
|
||||||
this.pruneStartTime(originalStartTime, cache.data);
|
|
||||||
}
|
|
||||||
return cache.data;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
console.error(err);
|
|
||||||
stateHistoryCache[cacheKey] = undefined;
|
|
||||||
});
|
|
||||||
cache.prom = prom;
|
|
||||||
cache.startTime = originalStartTime;
|
|
||||||
cache.endTime = endTime;
|
|
||||||
return prom;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRecent(entityId, startTime, endTime, localize, language) {
|
|
||||||
const cacheKey = entityId;
|
|
||||||
const cache = RECENT_CACHE[cacheKey];
|
|
||||||
|
|
||||||
if (
|
|
||||||
cache &&
|
|
||||||
Date.now() - cache.created < RECENT_THRESHOLD &&
|
|
||||||
cache.language === language
|
|
||||||
) {
|
|
||||||
return cache.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prom = this.fetchRecent(entityId, startTime, endTime).then(
|
|
||||||
(stateHistory) =>
|
|
||||||
computeHistory(this.hass, stateHistory, localize, language),
|
|
||||||
() => {
|
|
||||||
RECENT_CACHE[entityId] = false;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
RECENT_CACHE[cacheKey] = {
|
|
||||||
created: Date.now(),
|
|
||||||
language: language,
|
|
||||||
data: prom,
|
|
||||||
};
|
|
||||||
return prom;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchRecent(entityId, startTime, endTime, skipInitialState = false) {
|
|
||||||
let url = "history/period";
|
|
||||||
if (startTime) {
|
|
||||||
url += "/" + startTime.toISOString();
|
|
||||||
}
|
|
||||||
url += "?filter_entity_id=" + entityId;
|
|
||||||
if (endTime) {
|
|
||||||
url += "&end_time=" + endTime.toISOString();
|
|
||||||
}
|
|
||||||
if (skipInitialState) {
|
|
||||||
url += "&skip_initial_state";
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.hass.callApi("GET", url);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDate(startTime, endTime, localize, language) {
|
|
||||||
const filter =
|
|
||||||
startTime.toISOString() + "?end_time=" + endTime.toISOString();
|
|
||||||
|
|
||||||
const prom = this.hass
|
|
||||||
.callApi("GET", "history/period/" + filter)
|
|
||||||
.then(
|
|
||||||
(stateHistory) =>
|
|
||||||
computeHistory(this.hass, stateHistory, localize, language),
|
|
||||||
() => null
|
|
||||||
);
|
|
||||||
|
|
||||||
return prom;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customElements.define("ha-state-history-data", HaStateHistoryData);
|
customElements.define("ha-state-history-data", HaStateHistoryData);
|
||||||
|
225
src/data/history.ts
Normal file
225
src/data/history.ts
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import computeStateName from "../common/entity/compute_state_name.js";
|
||||||
|
import computeStateDomain from "../common/entity/compute_state_domain.js";
|
||||||
|
import computeStateDisplay from "../common/entity/compute_state_display.js";
|
||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { LocalizeFunc } from "../mixins/localize-base-mixin.js";
|
||||||
|
import { HomeAssistant } from "../types.js";
|
||||||
|
|
||||||
|
const DOMAINS_USE_LAST_UPDATED = ["climate", "water_heater"];
|
||||||
|
const LINE_ATTRIBUTES_TO_KEEP = [
|
||||||
|
"temperature",
|
||||||
|
"current_temperature",
|
||||||
|
"target_temp_low",
|
||||||
|
"target_temp_high",
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface LineChartState {
|
||||||
|
state: string;
|
||||||
|
last_changed: string;
|
||||||
|
attributes?: { [key: string]: any };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LineChartEntity {
|
||||||
|
domain: string;
|
||||||
|
name: string;
|
||||||
|
entity_id: string;
|
||||||
|
states: LineChartState[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LineChartUnit {
|
||||||
|
unit: string;
|
||||||
|
identifier: string;
|
||||||
|
data: LineChartEntity[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimelineState {
|
||||||
|
state_localize: string;
|
||||||
|
state: string;
|
||||||
|
last_changed: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimelineEntity {
|
||||||
|
name: string;
|
||||||
|
entity_id: string;
|
||||||
|
data: TimelineState[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HistoryResult {
|
||||||
|
line: LineChartUnit[];
|
||||||
|
timeline: TimelineEntity[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchRecent = (
|
||||||
|
hass,
|
||||||
|
entityId,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
skipInitialState = false
|
||||||
|
): Promise<HassEntity[][]> => {
|
||||||
|
let url = "history/period";
|
||||||
|
if (startTime) {
|
||||||
|
url += "/" + startTime.toISOString();
|
||||||
|
}
|
||||||
|
url += "?filter_entity_id=" + entityId;
|
||||||
|
if (endTime) {
|
||||||
|
url += "&end_time=" + endTime.toISOString();
|
||||||
|
}
|
||||||
|
if (skipInitialState) {
|
||||||
|
url += "&skip_initial_state";
|
||||||
|
}
|
||||||
|
|
||||||
|
return hass.callApi("GET", url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchDate = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
startTime: Date,
|
||||||
|
endTime: Date
|
||||||
|
): Promise<HassEntity[][]> => {
|
||||||
|
return hass.callApi(
|
||||||
|
"GET",
|
||||||
|
`history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const equalState = (obj1: LineChartState, obj2: LineChartState) =>
|
||||||
|
obj1.state === obj2.state &&
|
||||||
|
// They either both have an attributes object or not
|
||||||
|
(!obj1.attributes ||
|
||||||
|
LINE_ATTRIBUTES_TO_KEEP.every(
|
||||||
|
(attr) => obj1.attributes![attr] === obj2.attributes![attr]
|
||||||
|
));
|
||||||
|
|
||||||
|
const processTimelineEntity = (
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
language: string,
|
||||||
|
states: HassEntity[]
|
||||||
|
): TimelineEntity => {
|
||||||
|
const data: TimelineState[] = [];
|
||||||
|
|
||||||
|
for (const state of states) {
|
||||||
|
if (data.length > 0 && state.state === data[data.length - 1].state) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.push({
|
||||||
|
state_localize: computeStateDisplay(localize, state, language),
|
||||||
|
state: state.state,
|
||||||
|
last_changed: state.last_changed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: computeStateName(states[0]),
|
||||||
|
entity_id: states[0].entity_id,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const processLineChartEntities = (
|
||||||
|
unit,
|
||||||
|
entities: HassEntity[][]
|
||||||
|
): LineChartUnit => {
|
||||||
|
const data: LineChartEntity[] = [];
|
||||||
|
|
||||||
|
for (const states of entities) {
|
||||||
|
const last: HassEntity = states[states.length - 1];
|
||||||
|
const domain = computeStateDomain(last);
|
||||||
|
const processedStates: LineChartState[] = [];
|
||||||
|
|
||||||
|
for (const state of states) {
|
||||||
|
let processedState: LineChartState;
|
||||||
|
|
||||||
|
if (DOMAINS_USE_LAST_UPDATED.includes(domain)) {
|
||||||
|
processedState = {
|
||||||
|
state: state.state,
|
||||||
|
last_changed: state.last_updated,
|
||||||
|
attributes: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const attr of LINE_ATTRIBUTES_TO_KEEP) {
|
||||||
|
if (attr in state.attributes) {
|
||||||
|
processedState.attributes![attr] = state.attributes[attr];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
processedState = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
processedStates.length > 1 &&
|
||||||
|
equalState(
|
||||||
|
processedState,
|
||||||
|
processedStates[processedStates.length - 1]
|
||||||
|
) &&
|
||||||
|
equalState(processedState, processedStates[processedStates.length - 2])
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
processedStates.push(processedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.push({
|
||||||
|
domain,
|
||||||
|
name: computeStateName(last),
|
||||||
|
entity_id: last.entity_id,
|
||||||
|
states: processedStates,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
unit,
|
||||||
|
identifier: entities.map((states) => states[0].entity_id).join(""),
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const computeHistory = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
stateHistory: HassEntity[][],
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
language: string
|
||||||
|
): HistoryResult => {
|
||||||
|
const lineChartDevices: { [unit: string]: HassEntity[][] } = {};
|
||||||
|
const timelineDevices: TimelineEntity[] = [];
|
||||||
|
if (!stateHistory) {
|
||||||
|
return { line: [], timeline: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
stateHistory.forEach((stateInfo) => {
|
||||||
|
if (stateInfo.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateWithUnit = stateInfo.find(
|
||||||
|
(state) => "unit_of_measurement" in state.attributes
|
||||||
|
);
|
||||||
|
|
||||||
|
let unit: string | undefined;
|
||||||
|
|
||||||
|
if (stateWithUnit) {
|
||||||
|
unit = stateWithUnit.attributes.unit_of_measurement;
|
||||||
|
} else if (computeStateDomain(stateInfo[0]) === "climate") {
|
||||||
|
unit = hass.config.unit_system.temperature;
|
||||||
|
} else if (computeStateDomain(stateInfo[0]) === "water_heater") {
|
||||||
|
unit = hass.config.unit_system.temperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!unit) {
|
||||||
|
timelineDevices.push(
|
||||||
|
processTimelineEntity(localize, language, stateInfo)
|
||||||
|
);
|
||||||
|
} else if (unit in lineChartDevices) {
|
||||||
|
lineChartDevices[unit].push(stateInfo);
|
||||||
|
} else {
|
||||||
|
lineChartDevices[unit] = [stateInfo];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const unitStates = Object.keys(lineChartDevices).map((unit) =>
|
||||||
|
processLineChartEntities(unit, lineChartDevices[unit])
|
||||||
|
);
|
||||||
|
|
||||||
|
return { line: unitStates, timeline: timelineDevices };
|
||||||
|
};
|
@ -5,7 +5,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag.js";
|
|||||||
import { PolymerElement } from "@polymer/polymer/polymer-element.js";
|
import { PolymerElement } from "@polymer/polymer/polymer-element.js";
|
||||||
|
|
||||||
import "../../components/state-history-charts.js";
|
import "../../components/state-history-charts.js";
|
||||||
import "../../data/ha-state-history-data.js";
|
import "../../data/ha-state-history-data";
|
||||||
import "../../resources/ha-style.js";
|
import "../../resources/ha-style.js";
|
||||||
import "../../state-summary/state-card-content.js";
|
import "../../state-summary/state-card-content.js";
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import "@vaadin/vaadin-date-picker/vaadin-date-picker.js";
|
|||||||
|
|
||||||
import "../../components/ha-menu-button.js";
|
import "../../components/ha-menu-button.js";
|
||||||
import "../../components/state-history-charts.js";
|
import "../../components/state-history-charts.js";
|
||||||
import "../../data/ha-state-history-data.js";
|
import "../../data/ha-state-history-data";
|
||||||
import "../../resources/ha-date-picker-style.js";
|
import "../../resources/ha-date-picker-style.js";
|
||||||
import "../../resources/ha-style.js";
|
import "../../resources/ha-style.js";
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element.js";
|
|||||||
|
|
||||||
import "../../../components/ha-card.js";
|
import "../../../components/ha-card.js";
|
||||||
import "../../../components/state-history-charts.js";
|
import "../../../components/state-history-charts.js";
|
||||||
import "../../../data/ha-state-history-data.js";
|
import "../../../data/ha-state-history-data";
|
||||||
|
|
||||||
import processConfigEntities from "../common/process-config-entities.js";
|
import processConfigEntities from "../common/process-config-entities.js";
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user