20221005.0 (#14007)

This commit is contained in:
Bram Kragten 2022-10-05 16:42:29 +02:00 committed by GitHub
commit b327d6e0bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 645 additions and 539 deletions

View File

@ -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;
}
}

View File

@ -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(

View File

@ -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<string, StatisticValue[]> = {};
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;
}
);
};

385
demo/src/stubs/recorder.ts Normal file
View File

@ -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<string, StatisticValue[]> = {};
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;
}
);
};

View File

@ -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"

View File

@ -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);
}
`;

View File

@ -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;

View File

@ -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`<a
href=${manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${manifest.domain}`
)
: manifest.documentation}
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.config.integrations.config_flow.documentation"
)}</a
>`
: this.hass.localize(
"ui.panel.config.integrations.config_flow.documentation"
),
}
),
});
showYamlIntegrationDialog(this, { manifest });
}
private async _createFlow(integration: IntegrationListItem) {

View File

@ -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`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${this.hass.localize(
"ui.panel.config.integrations.config_flow.yaml_only_title"
)}
>
<p>
${this.hass.localize(
"ui.panel.config.integrations.config_flow.yaml_only"
)}
</p>
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
${this.hass.localize("ui.dialogs.generic.cancel")}
</mwc-button>
${docLink
? html`<a
.href=${docLink}
target="_blank"
rel="noreferrer noopener"
slot="primaryAction"
>
<mwc-button @click=${this.closeDialog} dialogInitialFocus>
${this.hass.localize(
"ui.panel.config.integrations.config_flow.open_documentation"
)}
</mwc-button>
</a>`
: html`<mwc-button @click=${this.closeDialog} dialogInitialFocus>
${this.hass.localize("ui.dialogs.generic.ok")}
</mwc-button>`}
</ha-dialog>
`;
}
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;
}
}

View File

@ -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`<mwc-list-item
graphic="medium"
.flow=${flow}
@click=${this._flowInProgressPicked}
@request-selected=${this._flowInProgressPicked}
hasMeta
>
<img
@ -80,7 +81,7 @@ class HaDomainIntegrations extends LitElement {
return html`<mwc-list-item
graphic="medium"
.domain=${domain}
@click=${this._standardPicked}
@request-selected=${this._standardPicked}
hasMeta
>
<img
@ -129,7 +130,7 @@ class HaDomainIntegrations extends LitElement {
is_built_in: val.is_built_in !== false,
cloud: val.iot_class?.startsWith("cloud_"),
}}
@click=${this._integrationPicked}
@request-selected=${this._integrationPicked}
>
</ha-integration-list-item>`
)
@ -138,7 +139,7 @@ class HaDomainIntegrations extends LitElement {
? html`<mwc-list-item
graphic="medium"
.domain=${this.domain}
@click=${this._standardPicked}
@request-selected=${this._standardPicked}
hasMeta
>
<img
@ -164,7 +165,7 @@ class HaDomainIntegrations extends LitElement {
? html`${this.flowsInProgress?.length
? html`<mwc-list-item
.domain=${this.domain}
@click=${this._integrationPicked}
@request-selected=${this._integrationPicked}
hasMeta
>
${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}
>
</ha-integration-list-item>`}`
: ""}
</mwc-list> `;
}
private async _integrationPicked(ev) {
const domain = ev.currentTarget.domain;
private async _integrationPicked(ev: CustomEvent<RequestSelectedDetail>) {
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`<a
href=${manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${manifest.domain}`
)
: manifest.documentation}
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.config.integrations.config_flow.documentation"
)}</a
>`
: 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<RequestSelectedDetail>) {
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<RequestSelectedDetail>) {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
const domain = (ev.currentTarget as any).domain;
const root = this.getRootNode();
fireEvent(this, "close-dialog");
protocolIntegrationPicked(

View File

@ -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,
});
};

View File

@ -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": {