diff --git a/demo/src/ha-demo.ts b/demo/src/ha-demo.ts index b80c4d71de..070fea8b44 100644 --- a/demo/src/ha-demo.ts +++ b/demo/src/ha-demo.ts @@ -20,6 +20,7 @@ import { mockHistory } from "./stubs/history"; import { mockLovelace } from "./stubs/lovelace"; import { mockMediaPlayer } from "./stubs/media_player"; import { mockPersistentNotification } from "./stubs/persistent_notification"; +import { mockRecorder } from "./stubs/recorder"; import { mockShoppingList } from "./stubs/shopping_list"; import { mockSystemLog } from "./stubs/system_log"; import { mockTemplate } from "./stubs/template"; @@ -45,6 +46,7 @@ class HaDemo extends HomeAssistantAppEl { mockAuth(hass); mockTranslations(hass); mockHistory(hass); + mockRecorder(hass); mockShoppingList(hass); mockSystemLog(hass); mockTemplate(hass); @@ -120,3 +122,9 @@ class HaDemo extends HomeAssistantAppEl { } customElements.define("ha-demo", HaDemo); + +declare global { + interface HTMLElementTagNameMap { + "ha-demo": HaDemo; + } +} diff --git a/demo/src/stubs/energy.ts b/demo/src/stubs/energy.ts index 80f10b968c..b459a9f95c 100644 --- a/demo/src/stubs/energy.ts +++ b/demo/src/stubs/energy.ts @@ -1,85 +1,101 @@ import { format, startOfToday, startOfTomorrow } from "date-fns/esm"; -import { EnergySolarForecasts } from "../../../src/data/energy"; +import { + EnergyInfo, + EnergyPreferences, + EnergySolarForecasts, + FossilEnergyConsumption, +} from "../../../src/data/energy"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; export const mockEnergy = (hass: MockHomeAssistant) => { - hass.mockWS("energy/get_prefs", () => ({ - energy_sources: [ - { - type: "grid", - flow_from: [ - { - stat_energy_from: "sensor.energy_consumption_tarif_1", - stat_cost: "sensor.energy_consumption_tarif_1_cost", - entity_energy_price: null, - number_energy_price: null, - }, - { - stat_energy_from: "sensor.energy_consumption_tarif_2", - stat_cost: "sensor.energy_consumption_tarif_2_cost", - entity_energy_price: null, - number_energy_price: null, - }, - ], - flow_to: [ - { - stat_energy_to: "sensor.energy_production_tarif_1", - stat_compensation: "sensor.energy_production_tarif_1_compensation", - entity_energy_price: null, - number_energy_price: null, - }, - { - stat_energy_to: "sensor.energy_production_tarif_2", - stat_compensation: "sensor.energy_production_tarif_2_compensation", - entity_energy_price: null, - number_energy_price: null, - }, - ], - cost_adjustment_day: 0, - }, - { - type: "solar", - stat_energy_from: "sensor.solar_production", - config_entry_solar_forecast: ["solar_forecast"], - }, - /* { - type: "battery", - stat_energy_from: "sensor.battery_output", - stat_energy_to: "sensor.battery_input", - }, */ - { - type: "gas", - stat_energy_from: "sensor.energy_gas", - stat_cost: "sensor.energy_gas_cost", - entity_energy_price: null, - number_energy_price: null, - }, - ], - device_consumption: [ - { - stat_consumption: "sensor.energy_car", - }, - { - stat_consumption: "sensor.energy_ac", - }, - { - stat_consumption: "sensor.energy_washing_machine", - }, - { - stat_consumption: "sensor.energy_dryer", - }, - { - stat_consumption: "sensor.energy_heat_pump", - }, - { - stat_consumption: "sensor.energy_boiler", - }, - ], - })); - hass.mockWS("energy/info", () => ({ cost_sensors: [] })); - hass.mockWS("energy/fossil_energy_consumption", ({ period }) => ({ - start: period === "month" ? 250 : period === "day" ? 10 : 2, - })); + hass.mockWS( + "energy/get_prefs", + (): EnergyPreferences => ({ + energy_sources: [ + { + type: "grid", + flow_from: [ + { + stat_energy_from: "sensor.energy_consumption_tarif_1", + stat_cost: "sensor.energy_consumption_tarif_1_cost", + entity_energy_price: null, + number_energy_price: null, + }, + { + stat_energy_from: "sensor.energy_consumption_tarif_2", + stat_cost: "sensor.energy_consumption_tarif_2_cost", + entity_energy_price: null, + number_energy_price: null, + }, + ], + flow_to: [ + { + stat_energy_to: "sensor.energy_production_tarif_1", + stat_compensation: + "sensor.energy_production_tarif_1_compensation", + entity_energy_price: null, + number_energy_price: null, + }, + { + stat_energy_to: "sensor.energy_production_tarif_2", + stat_compensation: + "sensor.energy_production_tarif_2_compensation", + entity_energy_price: null, + number_energy_price: null, + }, + ], + cost_adjustment_day: 0, + }, + { + type: "solar", + stat_energy_from: "sensor.solar_production", + config_entry_solar_forecast: ["solar_forecast"], + }, + /* { + type: "battery", + stat_energy_from: "sensor.battery_output", + stat_energy_to: "sensor.battery_input", + }, */ + { + type: "gas", + stat_energy_from: "sensor.energy_gas", + stat_cost: "sensor.energy_gas_cost", + entity_energy_price: null, + number_energy_price: null, + }, + ], + device_consumption: [ + { + stat_consumption: "sensor.energy_car", + }, + { + stat_consumption: "sensor.energy_ac", + }, + { + stat_consumption: "sensor.energy_washing_machine", + }, + { + stat_consumption: "sensor.energy_dryer", + }, + { + stat_consumption: "sensor.energy_heat_pump", + }, + { + stat_consumption: "sensor.energy_boiler", + }, + ], + }) + ); + hass.mockWS( + "energy/info", + (): EnergyInfo => ({ cost_sensors: {}, solar_forecast_domains: [] }) + ); + hass.mockWS( + "energy/fossil_energy_consumption", + ({ period }): FossilEnergyConsumption => ({ + start: period === "month" ? 250 : period === "day" ? 10 : 2, + }) + ); const todayString = format(startOfToday(), "yyyy-MM-dd"); const tomorrowString = format(startOfTomorrow(), "yyyy-MM-dd"); hass.mockWS( diff --git a/demo/src/stubs/history.ts b/demo/src/stubs/history.ts index 86f3445956..5b96b77ddf 100644 --- a/demo/src/stubs/history.ts +++ b/demo/src/stubs/history.ts @@ -1,12 +1,4 @@ -import { - addDays, - addHours, - addMonths, - differenceInHours, - endOfDay, -} from "date-fns/esm"; import { HassEntity } from "home-assistant-js-websocket"; -import { StatisticValue } from "../../../src/data/recorder"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; interface HistoryQueryParams { @@ -72,331 +64,6 @@ const generateHistory = (state, deltas) => { const incrementalUnits = ["clients", "queries", "ads"]; -const generateMeanStatistics = ( - id: string, - start: Date, - end: Date, - period: "5minute" | "hour" | "day" | "month" = "hour", - 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(), - end: 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 = - period === "day" - ? addDays(currentDate, 1) - : period === "month" - ? addMonths(currentDate, 1) - : addHours(currentDate, 1); - } - return statistics; -}; - -const generateSumStatistics = ( - id: string, - start: Date, - end: Date, - period: "5minute" | "hour" | "day" | "month" = "hour", - 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(), - end: currentDate.toISOString(), - mean: null, - min: null, - max: null, - last_reset: "1970-01-01T00:00:00+00:00", - state: initValue + sum, - sum, - }); - currentDate = - period === "day" - ? addDays(currentDate, 1) - : period === "month" - ? addMonths(currentDate, 1) - : addHours(currentDate, 1); - } - return statistics; -}; - -const generateCurvedStatistics = ( - id: string, - start: Date, - end: Date, - _period: "5minute" | "hour" | "day" | "month" = "hour", - 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(), - end: 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, - period: "5minute" | "hour" | "day" | "month" - ) => StatisticValue[] -> = { - "sensor.energy_consumption_tarif_1": ( - id: string, - start: Date, - end: Date, - period = "hour" - ) => { - if (period !== "hour") { - return generateSumStatistics( - id, - start, - end, - period, - 0, - period === "day" ? 17 : 504 - ); - } - const morningEnd = new Date(start.getTime() + 10 * 60 * 60 * 1000); - const morningLow = generateSumStatistics( - id, - start, - morningEnd, - period, - 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, - period, - morningFinalVal, - 0 - ); - const eveningLow = generateSumStatistics( - id, - eveningStart, - end, - period, - morningFinalVal, - 0.7 - ); - return [...morningLow, ...empty, ...eveningLow]; - }, - "sensor.energy_consumption_tarif_2": ( - id: string, - start: Date, - end: Date, - period = "hour" - ) => { - if (period !== "hour") { - return generateSumStatistics( - id, - start, - end, - period, - 0, - period === "day" ? 17 : 504 - ); - } - 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, - period, - 0, - 0.3 - ); - const highTarifFinalVal = highTarif.length - ? highTarif[highTarif.length - 1].sum! - : 0; - const morning = generateSumStatistics(id, start, morningEnd, period, 0, 0); - const evening = generateSumStatistics( - id, - eveningStart, - end, - period, - highTarifFinalVal, - 0 - ); - return [...morning, ...highTarif, ...evening]; - }, - "sensor.energy_production_tarif_1": (id, start, end, period = "hour") => - generateSumStatistics(id, start, end, period, 0, 0), - "sensor.energy_production_tarif_1_compensation": ( - id, - start, - end, - period = "hour" - ) => generateSumStatistics(id, start, end, period, 0, 0), - "sensor.energy_production_tarif_2": (id, start, end, period = "hour") => { - if (period !== "hour") { - return generateSumStatistics( - id, - start, - end, - period, - 0, - period === "day" ? 17 : 504 - ); - } - 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, - period, - 0, - 0.15, - true - ); - const productionFinalVal = production.length - ? production[production.length - 1].sum! - : 0; - const morning = generateSumStatistics( - id, - start, - productionStart, - period, - 0, - 0 - ); - const evening = generateSumStatistics( - id, - productionEnd, - dayEnd, - period, - productionFinalVal, - 0 - ); - const rest = generateSumStatistics( - id, - dayEnd, - end, - period, - productionFinalVal, - 1 - ); - return [...morning, ...production, ...evening, ...rest]; - }, - "sensor.solar_production": (id, start, end, period = "hour") => { - if (period !== "hour") { - return generateSumStatistics( - id, - start, - end, - period, - 0, - period === "day" ? 17 : 504 - ); - } - 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, - period, - 0, - 0.3, - true - ); - const productionFinalVal = production.length - ? production[production.length - 1].sum! - : 0; - const morning = generateSumStatistics( - id, - start, - productionStart, - period, - 0, - 0 - ); - const evening = generateSumStatistics( - id, - productionEnd, - dayEnd, - period, - productionFinalVal, - 0 - ); - const rest = generateSumStatistics( - id, - dayEnd, - end, - period, - productionFinalVal, - 2 - ); - return [...morning, ...production, ...evening, ...rest]; - }, -}; - export const mockHistory = (mockHass: MockHomeAssistant) => { mockHass.mockAPI( new RegExp("history/period/.+"), @@ -466,43 +133,4 @@ export const mockHistory = (mockHass: MockHomeAssistant) => { return results; } ); - mockHass.mockWS("recorder/get_statistics_metadata", () => []); - mockHass.mockWS("history/list_statistic_ids", () => []); - mockHass.mockWS( - "history/statistics_during_period", - ({ statistic_ids, start_time, end_time, period }, 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, period); - } 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, - period, - state, - state * (state > 80 ? 0.01 : 0.05) - ) - : generateMeanStatistics( - id, - start, - end, - period, - state, - state * (state > 80 ? 0.05 : 0.1) - ); - } - }); - return statistics; - } - ); }; diff --git a/demo/src/stubs/recorder.ts b/demo/src/stubs/recorder.ts new file mode 100644 index 0000000000..9472772ce1 --- /dev/null +++ b/demo/src/stubs/recorder.ts @@ -0,0 +1,385 @@ +import { + addDays, + addHours, + addMonths, + differenceInHours, + endOfDay, +} from "date-fns"; +import { + Statistics, + StatisticsMetaData, + StatisticValue, +} from "../../../src/data/recorder"; +import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; + +const generateMeanStatistics = ( + id: string, + start: Date, + end: Date, + period: "5minute" | "hour" | "day" | "month" = "hour", + initValue: number, + maxDiff: number +): StatisticValue[] => { + 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(), + end: 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 = + period === "day" + ? addDays(currentDate, 1) + : period === "month" + ? addMonths(currentDate, 1) + : addHours(currentDate, 1); + } + return statistics; +}; + +const generateSumStatistics = ( + id: string, + start: Date, + end: Date, + period: "5minute" | "hour" | "day" | "month" = "hour", + initValue: number, + maxDiff: number +): StatisticValue[] => { + 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(), + end: currentDate.toISOString(), + mean: null, + min: null, + max: null, + last_reset: "1970-01-01T00:00:00+00:00", + state: initValue + sum, + sum, + }); + currentDate = + period === "day" + ? addDays(currentDate, 1) + : period === "month" + ? addMonths(currentDate, 1) + : addHours(currentDate, 1); + } + return statistics; +}; + +const generateCurvedStatistics = ( + id: string, + start: Date, + end: Date, + _period: "5minute" | "hour" | "day" | "month" = "hour", + initValue: number, + maxDiff: number, + metered: boolean +): StatisticValue[] => { + 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(), + end: 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, + period: "5minute" | "hour" | "day" | "month" + ) => StatisticValue[] +> = { + "sensor.energy_consumption_tarif_1": ( + id: string, + start: Date, + end: Date, + period = "hour" + ) => { + if (period !== "hour") { + return generateSumStatistics( + id, + start, + end, + period, + 0, + period === "day" ? 17 : 504 + ); + } + const morningEnd = new Date(start.getTime() + 10 * 60 * 60 * 1000); + const morningLow = generateSumStatistics( + id, + start, + morningEnd, + period, + 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, + period, + morningFinalVal, + 0 + ); + const eveningLow = generateSumStatistics( + id, + eveningStart, + end, + period, + morningFinalVal, + 0.7 + ); + return [...morningLow, ...empty, ...eveningLow]; + }, + "sensor.energy_consumption_tarif_2": ( + id: string, + start: Date, + end: Date, + period = "hour" + ) => { + if (period !== "hour") { + return generateSumStatistics( + id, + start, + end, + period, + 0, + period === "day" ? 17 : 504 + ); + } + 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, + period, + 0, + 0.3 + ); + const highTarifFinalVal = highTarif.length + ? highTarif[highTarif.length - 1].sum! + : 0; + const morning = generateSumStatistics(id, start, morningEnd, period, 0, 0); + const evening = generateSumStatistics( + id, + eveningStart, + end, + period, + highTarifFinalVal, + 0 + ); + return [...morning, ...highTarif, ...evening]; + }, + "sensor.energy_production_tarif_1": (id, start, end, period = "hour") => + generateSumStatistics(id, start, end, period, 0, 0), + "sensor.energy_production_tarif_1_compensation": ( + id, + start, + end, + period = "hour" + ) => generateSumStatistics(id, start, end, period, 0, 0), + "sensor.energy_production_tarif_2": (id, start, end, period = "hour") => { + if (period !== "hour") { + return generateSumStatistics( + id, + start, + end, + period, + 0, + period === "day" ? 17 : 504 + ); + } + 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, + period, + 0, + 0.15, + true + ); + const productionFinalVal = production.length + ? production[production.length - 1].sum! + : 0; + const morning = generateSumStatistics( + id, + start, + productionStart, + period, + 0, + 0 + ); + const evening = generateSumStatistics( + id, + productionEnd, + dayEnd, + period, + productionFinalVal, + 0 + ); + const rest = generateSumStatistics( + id, + dayEnd, + end, + period, + productionFinalVal, + 1 + ); + return [...morning, ...production, ...evening, ...rest]; + }, + "sensor.solar_production": (id, start, end, period = "hour") => { + if (period !== "hour") { + return generateSumStatistics( + id, + start, + end, + period, + 0, + period === "day" ? 17 : 504 + ); + } + 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, + period, + 0, + 0.3, + true + ); + const productionFinalVal = production.length + ? production[production.length - 1].sum! + : 0; + const morning = generateSumStatistics( + id, + start, + productionStart, + period, + 0, + 0 + ); + const evening = generateSumStatistics( + id, + productionEnd, + dayEnd, + period, + productionFinalVal, + 0 + ); + const rest = generateSumStatistics( + id, + dayEnd, + end, + period, + productionFinalVal, + 2 + ); + return [...morning, ...production, ...evening, ...rest]; + }, +}; +export const mockRecorder = (mockHass: MockHomeAssistant) => { + mockHass.mockWS( + "recorder/get_statistics_metadata", + (): StatisticsMetaData[] => [] + ); + mockHass.mockWS( + "recorder/list_statistic_ids", + (): StatisticsMetaData[] => [] + ); + mockHass.mockWS( + "recorder/statistics_during_period", + ({ statistic_ids, start_time, end_time, period }, hass): Statistics => { + 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, period); + } 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, + period, + state, + state * (state > 80 ? 0.01 : 0.05) + ) + : generateMeanStatistics( + id, + start, + end, + period, + state, + state * (state > 80 ? 0.05 : 0.1) + ); + } + }); + return statistics; + } + ); +}; diff --git a/pyproject.toml b/pyproject.toml index 14b67e7416..b632b56980 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20221004.0" +version = "20221005.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/src/components/ha-selector/ha-selector-text.ts b/src/components/ha-selector/ha-selector-text.ts index 7af6c36d27..dd47b2c27c 100644 --- a/src/components/ha-selector/ha-selector-text.ts +++ b/src/components/ha-selector/ha-selector-text.ts @@ -98,13 +98,13 @@ export class HaTextSelector extends LitElement { } ha-icon-button { position: absolute; - top: 16px; - right: 16px; - --mdc-icon-button-size: 24px; + top: 10px; + right: 10px; + --mdc-icon-button-size: 36px; --mdc-icon-size: 20px; color: var(--secondary-text-color); inset-inline-start: initial; - inset-inline-end: 16px; + inset-inline-end: 10px; direction: var(--direction); } `; diff --git a/src/data/energy.ts b/src/data/energy.ts index 80dcfbb511..be4dafffe0 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -618,7 +618,7 @@ export const getEnergyGasUnitClass = ( const statisticIdWithMeta = statisticsMetaData[source.stat_energy_from]; if ( energyGasUnitClass.includes( - statisticIdWithMeta.unit_class as EnergyGasUnitClass + statisticIdWithMeta?.unit_class as EnergyGasUnitClass ) ) { return statisticIdWithMeta.unit_class as EnergyGasUnitClass; diff --git a/src/panels/config/integrations/dialog-add-integration.ts b/src/panels/config/integrations/dialog-add-integration.ts index a775b05506..865cabbd5d 100644 --- a/src/panels/config/integrations/dialog-add-integration.ts +++ b/src/panels/config/integrations/dialog-add-integration.ts @@ -36,10 +36,12 @@ import { } from "../../../dialogs/generic/show-dialog-box"; import { haStyleDialog, haStyleScrollbar } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; -import { documentationUrl } from "../../../util/documentation-url"; import "./ha-domain-integrations"; import "./ha-integration-list-item"; -import { AddIntegrationDialogParams } from "./show-add-integration-dialog"; +import { + AddIntegrationDialogParams, + showYamlIntegrationDialog, +} from "./show-add-integration-dialog"; export interface IntegrationListItem { name: string; @@ -545,35 +547,7 @@ class AddIntegrationDialog extends LitElement { this.hass, integration.domain ); - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.integrations.config_flow.yaml_only_title" - ), - text: this.hass.localize( - "ui.panel.config.integrations.config_flow.yaml_only_text", - { - link: - manifest?.is_built_in || manifest?.documentation - ? html` - ${this.hass.localize( - "ui.panel.config.integrations.config_flow.documentation" - )}` - : this.hass.localize( - "ui.panel.config.integrations.config_flow.documentation" - ), - } - ), - }); + showYamlIntegrationDialog(this, { manifest }); } private async _createFlow(integration: IntegrationListItem) { diff --git a/src/panels/config/integrations/dialog-yaml-integration.ts b/src/panels/config/integrations/dialog-yaml-integration.ts new file mode 100644 index 0000000000..932c955bde --- /dev/null +++ b/src/panels/config/integrations/dialog-yaml-integration.ts @@ -0,0 +1,96 @@ +import "@material/mwc-button/mwc-button"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { HomeAssistant } from "../../../types"; +import { documentationUrl } from "../../../util/documentation-url"; +import { YamlIntegrationDialogParams } from "./show-add-integration-dialog"; + +@customElement("dialog-yaml-integration") +export class DialogYamlIntegration extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: YamlIntegrationDialogParams; + + public showDialog(params: YamlIntegrationDialogParams): void { + this._params = params; + } + + public closeDialog() { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._params) { + return html``; + } + const manifest = this._params.manifest; + const docLink = manifest.is_built_in + ? documentationUrl(this.hass, `/integrations/${manifest.domain}`) + : manifest.documentation; + return html` + +

+ ${this.hass.localize( + "ui.panel.config.integrations.config_flow.yaml_only" + )} +

+ + ${this.hass.localize("ui.dialogs.generic.cancel")} + + ${docLink + ? html` + + ${this.hass.localize( + "ui.panel.config.integrations.config_flow.open_documentation" + )} + + ` + : html` + ${this.hass.localize("ui.dialogs.generic.ok")} + `} +
+ `; + } + + static get styles(): CSSResultGroup { + return css` + :host([inert]) { + pointer-events: initial !important; + cursor: initial !important; + } + a { + text-decoration: none; + } + ha-dialog { + --mdc-dialog-heading-ink-color: var(--primary-text-color); + --mdc-dialog-content-ink-color: var(--primary-text-color); + /* Place above other dialogs */ + --dialog-z-index: 104; + } + @media all and (min-width: 600px) { + ha-dialog { + --mdc-dialog-min-width: 400px; + } + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-yaml-integration": DialogYamlIntegration; + } +} diff --git a/src/panels/config/integrations/ha-domain-integrations.ts b/src/panels/config/integrations/ha-domain-integrations.ts index 45a2716ac0..6b9a89e73d 100644 --- a/src/panels/config/integrations/ha-domain-integrations.ts +++ b/src/panels/config/integrations/ha-domain-integrations.ts @@ -1,8 +1,10 @@ +import { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item-base"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { fireEvent } from "../../../common/dom/fire_event"; import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked"; +import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import { navigate } from "../../../common/navigate"; import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import { localizeConfigFlowTitle } from "../../../data/config_flow"; @@ -13,12 +15,11 @@ import { } from "../../../data/integration"; import { Integration } from "../../../data/integrations"; import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; -import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; -import { documentationUrl } from "../../../util/documentation-url"; import "./ha-integration-list-item"; +import { showYamlIntegrationDialog } from "./show-add-integration-dialog"; const standardToDomain = { zigbee: "zha", zwave: "zwave_js" } as const; @@ -43,7 +44,7 @@ class HaDomainIntegrations extends LitElement { (flow) => html` ` ) @@ -138,7 +139,7 @@ class HaDomainIntegrations extends LitElement { ? html` ${this.hass.localize("ui.panel.config.integrations.new_flow", { @@ -186,15 +187,18 @@ class HaDomainIntegrations extends LitElement { is_built_in: this.integration.is_built_in !== false, cloud: this.integration.iot_class?.startsWith("cloud_"), }} - @click=${this._integrationPicked} + @request-selected=${this._integrationPicked} > `}` : ""} `; } - private async _integrationPicked(ev) { - const domain = ev.currentTarget.domain; + private async _integrationPicked(ev: CustomEvent) { + if (!shouldHandleRequestSelectedEvent(ev)) { + return; + } + const domain = (ev.currentTarget as any).domain; if ( ["cloud", "google_assistant", "alexa"].includes(domain) && @@ -207,43 +211,16 @@ class HaDomainIntegrations extends LitElement { if ( (domain === this.domain && - !this.integration!.config_flow && - (!this.integration!.integrations?.[domain] || - !this.integration!.integrations[domain].config_flow)) || - !this.integration!.integrations?.[domain]?.config_flow + (!this.integration!.integrations || + !(domain in this.integration!.integrations)) && + !this.integration!.config_flow) || + this.integration!.integrations?.[domain]?.config_flow === false ) { const manifest = await fetchIntegrationManifest(this.hass, domain); - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.integrations.config_flow.yaml_only_title" - ), - text: this.hass.localize( - "ui.panel.config.integrations.config_flow.yaml_only_text", - { - link: - manifest?.is_built_in || manifest?.documentation - ? html` - ${this.hass.localize( - "ui.panel.config.integrations.config_flow.documentation" - )}` - : this.hass.localize( - "ui.panel.config.integrations.config_flow.documentation" - ), - } - ), - }); + showYamlIntegrationDialog(this, { manifest }); return; } + const root = this.getRootNode(); showConfigFlowDialog( root instanceof ShadowRoot ? (root.host as HTMLElement) : this, @@ -256,8 +233,11 @@ class HaDomainIntegrations extends LitElement { fireEvent(this, "close-dialog"); } - private async _flowInProgressPicked(ev) { - const flow: DataEntryFlowProgress = ev.currentTarget.flow; + private async _flowInProgressPicked(ev: CustomEvent) { + if (!shouldHandleRequestSelectedEvent(ev)) { + return; + } + const flow: DataEntryFlowProgress = (ev.currentTarget as any).flow; const root = this.getRootNode(); showConfigFlowDialog( root instanceof ShadowRoot ? (root.host as HTMLElement) : this, @@ -270,8 +250,11 @@ class HaDomainIntegrations extends LitElement { fireEvent(this, "close-dialog"); } - private _standardPicked(ev) { - const domain = ev.currentTarget.domain; + private _standardPicked(ev: CustomEvent) { + if (!shouldHandleRequestSelectedEvent(ev)) { + return; + } + const domain = (ev.currentTarget as any).domain; const root = this.getRootNode(); fireEvent(this, "close-dialog"); protocolIntegrationPicked( diff --git a/src/panels/config/integrations/show-add-integration-dialog.ts b/src/panels/config/integrations/show-add-integration-dialog.ts index 1530b73ce6..0bfbf0cb76 100644 --- a/src/panels/config/integrations/show-add-integration-dialog.ts +++ b/src/panels/config/integrations/show-add-integration-dialog.ts @@ -1,10 +1,15 @@ import { fireEvent } from "../../../common/dom/fire_event"; +import { IntegrationManifest } from "../../../data/integration"; export interface AddIntegrationDialogParams { brand?: string; initialFilter?: string; } +export interface YamlIntegrationDialogParams { + manifest: IntegrationManifest; +} + export const showAddIntegrationDialog = ( element: HTMLElement, dialogParams?: AddIntegrationDialogParams @@ -15,3 +20,14 @@ export const showAddIntegrationDialog = ( dialogParams: dialogParams, }); }; + +export const showYamlIntegrationDialog = ( + element: HTMLElement, + dialogParams?: YamlIntegrationDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-yaml-integration", + dialogImport: () => import("./dialog-yaml-integration"), + dialogParams: dialogParams, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 4494ffae8a..b2c6fbd067 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2965,8 +2965,8 @@ "next": "Next", "found_following_devices": "We found the following devices", "yaml_only_title": "This device can not be added from the UI", - "yaml_only_text": "You can add this device by adding it to your `configuration.yaml`. See the {link} for more information.", - "documentation": "documentation", + "yaml_only": "You can add this device by adding it to your ''configuration.yaml''. See the documentation for more information.", + "open_documentation": "Open documentation", "no_config_flow": "This integration does not support configuration via the UI. If you followed this link from the Home Assistant website, make sure you run the latest version of Home Assistant.", "not_all_required_fields": "Not all required fields are filled in.", "error_saving_area": "Error saving area: {error}", @@ -4594,10 +4594,10 @@ "title": "YAML", "section": { "validation": { - "heading": "Configuration validation", - "introduction": "Validate your configuration if you recently made some changes to it and want to make sure that it is all valid.", + "heading": "Check and Restart", + "introduction": "A basic validation of the configuration is automatically done before restarting. The basic validation ensures the YAML configuration doesn't have errors which will prevent Home Assistant or any integration from starting. It's also possible to only do the basic validation check without restarting.", "check_config": "Check configuration", - "valid": "Configuration valid!", + "valid": "Configuration will not prevent Home Assistant from starting!", "invalid": "Configuration invalid!" }, "reloading": {