import { addHours, differenceInHours, endOfDay } from "date-fns"; import { HassEntity } from "home-assistant-js-websocket"; import { StatisticValue } from "../../../src/data/history"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; interface HistoryQueryParams { filter_entity_id: string; end_time: string; } const parseQuery = (queryString: string) => { const query: any = {}; const items = queryString.split("&"); for (const item of items) { const parts = item.split("="); const key = decodeURIComponent(parts[0]); const value = parts.length > 1 ? decodeURIComponent(parts[1]) : undefined; query[key] = value; } return query as T; }; const getTime = (minutesAgo) => { const ts = new Date(Date.now() - minutesAgo * 60 * 1000); return ts.toISOString(); }; const randomTimeAdjustment = (diff) => Math.random() * diff - diff / 2; const maxTime = 1440; const generateHistory = (state, deltas) => { const changes = typeof deltas[0] === "object" ? deltas : deltas.map((st) => ({ state: st })); const timeDiff = 900 / changes.length; return changes.map((change, index) => { let attributes; if (!change.attributes && !state.attributes) { attributes = {}; } else if (!change.attributes) { attributes = state.attributes; } else if (!state.attributes) { attributes = change.attributes; } else { attributes = { ...state.attributes, ...change.attributes }; } const time = index === 0 ? getTime(maxTime) : getTime(maxTime - index * timeDiff + randomTimeAdjustment(timeDiff)); return { attributes, entity_id: state.entity_id, state: change.state || state.state, last_changed: time, last_updated: time, }; }); }; const incrementalUnits = ["clients", "queries", "ads"]; const generateMeanStatistics = ( id: string, start: Date, end: Date, initValue: number, maxDiff: number ) => { const statistics: StatisticValue[] = []; let currentDate = new Date(start); currentDate.setMinutes(0, 0, 0); let lastVal = initValue; const now = new Date(); while (end > currentDate && currentDate < now) { const delta = Math.random() * maxDiff; const mean = lastVal + delta; statistics.push({ statistic_id: id, start: currentDate.toISOString(), mean, min: mean - Math.random() * maxDiff, max: mean + Math.random() * maxDiff, last_reset: "1970-01-01T00:00:00+00:00", state: mean, sum: null, }); lastVal = mean; currentDate = addHours(currentDate, 1); } return statistics; }; const generateSumStatistics = ( id: string, start: Date, end: Date, initValue: number, maxDiff: number ) => { const statistics: StatisticValue[] = []; let currentDate = new Date(start); currentDate.setMinutes(0, 0, 0); let sum = initValue; const now = new Date(); while (end > currentDate && currentDate < now) { const add = Math.random() * maxDiff; sum += add; statistics.push({ statistic_id: id, start: currentDate.toISOString(), mean: null, min: null, max: null, last_reset: "1970-01-01T00:00:00+00:00", state: initValue + sum, sum, }); currentDate = addHours(currentDate, 1); } return statistics; }; const generateCurvedStatistics = ( id: string, start: Date, end: Date, initValue: number, maxDiff: number, metered: boolean ) => { const statistics: StatisticValue[] = []; let currentDate = new Date(start); currentDate.setMinutes(0, 0, 0); let sum = initValue; const hours = differenceInHours(end, start) - 1; let i = 0; let half = false; const now = new Date(); while (end > currentDate && currentDate < now) { const add = Math.random() * maxDiff; sum += i * add; statistics.push({ statistic_id: id, start: currentDate.toISOString(), mean: null, min: null, max: null, last_reset: "1970-01-01T00:00:00+00:00", state: initValue + sum, sum: metered ? sum : null, }); currentDate = addHours(currentDate, 1); if (!half && i > hours / 2) { half = true; } i += half ? -1 : 1; } return statistics; }; const statisticsFunctions: Record< string, (id: string, start: Date, end: Date) => StatisticValue[] > = { "sensor.energy_consumption_tarif_1": (id: string, start: Date, end: Date) => { const morningEnd = new Date(start.getTime() + 10 * 60 * 60 * 1000); const morningLow = generateSumStatistics(id, start, morningEnd, 0, 0.7); const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000); const morningFinalVal = morningLow.length ? morningLow[morningLow.length - 1].sum! : 0; const empty = generateSumStatistics( id, morningEnd, eveningStart, morningFinalVal, 0 ); const eveningLow = generateSumStatistics( id, eveningStart, end, morningFinalVal, 0.7 ); return [...morningLow, ...empty, ...eveningLow]; }, "sensor.energy_consumption_tarif_2": (id: string, start: Date, end: Date) => { const morningEnd = new Date(start.getTime() + 9 * 60 * 60 * 1000); const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000); const highTarif = generateSumStatistics( id, morningEnd, eveningStart, 0, 0.3 ); const highTarifFinalVal = highTarif.length ? highTarif[highTarif.length - 1].sum! : 0; const morning = generateSumStatistics(id, start, morningEnd, 0, 0); const evening = generateSumStatistics( id, eveningStart, end, highTarifFinalVal, 0 ); return [...morning, ...highTarif, ...evening]; }, "sensor.energy_production_tarif_1": (id, start, end) => generateSumStatistics(id, start, end, 0, 0), "sensor.energy_production_tarif_1_compensation": (id, start, end) => generateSumStatistics(id, start, end, 0, 0), "sensor.energy_production_tarif_2": (id, start, end) => { const productionStart = new Date(start.getTime() + 9 * 60 * 60 * 1000); const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000); const dayEnd = new Date(endOfDay(productionEnd)); const production = generateCurvedStatistics( id, productionStart, productionEnd, 0, 0.15, true ); const productionFinalVal = production.length ? production[production.length - 1].sum! : 0; const morning = generateSumStatistics(id, start, productionStart, 0, 0); const evening = generateSumStatistics( id, productionEnd, dayEnd, productionFinalVal, 0 ); const rest = generateSumStatistics(id, dayEnd, end, productionFinalVal, 1); return [...morning, ...production, ...evening, ...rest]; }, "sensor.solar_production": (id, start, end) => { const productionStart = new Date(start.getTime() + 7 * 60 * 60 * 1000); const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000); const dayEnd = new Date(endOfDay(productionEnd)); const production = generateCurvedStatistics( id, productionStart, productionEnd, 0, 0.3, true ); const productionFinalVal = production.length ? production[production.length - 1].sum! : 0; const morning = generateSumStatistics(id, start, productionStart, 0, 0); const evening = generateSumStatistics( id, productionEnd, dayEnd, productionFinalVal, 0 ); const rest = generateSumStatistics(id, dayEnd, end, productionFinalVal, 2); return [...morning, ...production, ...evening, ...rest]; }, "sensor.grid_fossil_fuel_percentage": (id, start, end) => generateMeanStatistics(id, start, end, 35, 1.3), }; export const mockHistory = (mockHass: MockHomeAssistant) => { mockHass.mockAPI( new RegExp("history/period/.+"), (hass, _method, path, _parameters) => { const params = parseQuery(path.split("?")[1]); const entities = params.filter_entity_id.split(","); const results: HassEntity[][] = []; for (const entityId of entities) { const state = hass.states[entityId]; if (!state) { continue; } if (!state.attributes.unit_of_measurement) { results.push(generateHistory(state, [state.state])); continue; } const numberState = Number(state.state); if (isNaN(numberState)) { // eslint-disable-next-line no-console console.log( "Ignoring state with unparsable state but with a unit", entityId, state ); continue; } const statesToGenerate = 15; let genFunc; if (incrementalUnits.includes(state.attributes.unit_of_measurement)) { let initial = Math.floor( numberState * 0.4 + numberState * Math.random() * 0.2 ); const diff = Math.max( 1, Math.floor((numberState - initial) / statesToGenerate) ); genFunc = () => { initial += diff; return Math.min(numberState, initial); }; } else { const diff = Math.floor( numberState * (numberState > 80 ? 0.05 : 0.5) ); genFunc = () => numberState - diff + Math.floor(Math.random() * 2 * diff); } results.push( generateHistory( { entity_id: state.entity_id, attributes: state.attributes, }, Array.from({ length: statesToGenerate }, genFunc) ) ); } return results; } ); mockHass.mockWS("history/list_statistic_ids", () => []); mockHass.mockWS( "history/statistics_during_period", ({ statistic_ids, start_time, end_time }, hass) => { const start = new Date(start_time); const end = end_time ? new Date(end_time) : new Date(); const statistics: Record = {}; statistic_ids.forEach((id: string) => { if (id in statisticsFunctions) { statistics[id] = statisticsFunctions[id](id, start, end); } else { const entityState = hass.states[id]; const state = entityState ? Number(entityState.state) : 1; statistics[id] = entityState && "last_reset" in entityState.attributes ? generateSumStatistics( id, start, end, state, state * (state > 80 ? 0.01 : 0.05) ) : generateMeanStatistics( id, start, end, state, state * (state > 80 ? 0.05 : 0.1) ); } }); return statistics; } ); };