mirror of
https://github.com/home-assistant/frontend.git
synced 2025-04-24 13:27:22 +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 "../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 EventsMixin from "../mixins/events-mixin.js";
|
||||
|
@ -255,7 +255,7 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
|
||||
Array.prototype.push.apply(datasets, data);
|
||||
});
|
||||
|
||||
const formatTooltipTitle = function(items, data) {
|
||||
const formatTooltipTitle = (items, data) => {
|
||||
const item = items[0];
|
||||
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 { 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";
|
||||
|
||||
const RECENT_THRESHOLD = 60000; // 1 minute
|
||||
const RECENT_CACHE = {};
|
||||
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 };
|
||||
}
|
||||
import { computeHistory, fetchDate } from "./history";
|
||||
import { getRecent, getRecentWithCache } from "./cached-history";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
@ -225,7 +115,10 @@ class HaStateHistoryData extends LocalizeMixin(PolymerElement) {
|
||||
|
||||
if (filterType === "date") {
|
||||
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") {
|
||||
if (!entityId) return;
|
||||
if (cacheConfig) {
|
||||
@ -236,7 +129,14 @@ class HaStateHistoryData extends LocalizeMixin(PolymerElement) {
|
||||
language
|
||||
);
|
||||
} else {
|
||||
data = this.getRecent(entityId, startTime, endTime, localize, language);
|
||||
data = getRecent(
|
||||
this.hass,
|
||||
entityId,
|
||||
startTime,
|
||||
endTime,
|
||||
localize,
|
||||
language
|
||||
);
|
||||
}
|
||||
} else {
|
||||
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) {
|
||||
if (this._refreshTimeoutId) {
|
||||
window.clearInterval(this._refreshTimeoutId);
|
||||
@ -264,193 +156,24 @@ class HaStateHistoryData extends LocalizeMixin(PolymerElement) {
|
||||
}
|
||||
if (cacheConfig.refresh) {
|
||||
this._refreshTimeoutId = window.setInterval(() => {
|
||||
this.getRecentWithCache(entityId, cacheConfig, localize, language).then(
|
||||
(stateHistory) => {
|
||||
this._setData(Object.assign({}, stateHistory));
|
||||
}
|
||||
);
|
||||
getRecentWithCache(
|
||||
this.hass,
|
||||
entityId,
|
||||
cacheConfig,
|
||||
localize,
|
||||
language
|
||||
).then((stateHistory) => {
|
||||
this._setData(Object.assign({}, stateHistory));
|
||||
});
|
||||
}, cacheConfig.refresh * 1000);
|
||||
}
|
||||
return this.getRecentWithCache(entityId, cacheConfig, localize, language);
|
||||
}
|
||||
|
||||
mergeLine(historyLines, cacheLines) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
return getRecentWithCache(
|
||||
this.hass,
|
||||
entityId,
|
||||
cacheConfig,
|
||||
localize,
|
||||
language
|
||||
);
|
||||
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);
|
||||
|
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 "../../components/state-history-charts.js";
|
||||
import "../../data/ha-state-history-data.js";
|
||||
import "../../data/ha-state-history-data";
|
||||
import "../../resources/ha-style.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/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-style.js";
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element.js";
|
||||
|
||||
import "../../../components/ha-card.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";
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user