mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-13 04:36:53 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8af5908682 | |||
| 60e95b886c | |||
| 0385ca8076 | |||
| 02c65fc8cb | |||
| 49290d5c83 | |||
| 08aff3bfd7 | |||
| 455fa45b9c | |||
| 2e56a4ec4c | |||
| 76131ff09e | |||
| 89d8723c5a | |||
| 7bdb63a6fe | |||
| eed79f1797 | |||
| 76665009da | |||
| 6d7d08fddc | |||
| 77d4e6dc43 | |||
| 7345256b30 | |||
| e0d98e95fa | |||
| 17041044cf |
@@ -1,3 +1,4 @@
|
||||
/* global process */
|
||||
// Tasks to generate entry HTML
|
||||
|
||||
import {
|
||||
@@ -25,6 +26,7 @@ const SAFARI_TO_MACOS = {
|
||||
16: [11, 0, 0],
|
||||
17: [12, 0, 0],
|
||||
18: [13, 0, 0],
|
||||
26: [26, 0, 0],
|
||||
};
|
||||
|
||||
const getCommonTemplateVars = () => {
|
||||
|
||||
+4
-4
@@ -137,11 +137,11 @@
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.1",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.60.0",
|
||||
"@lokalise/node-api": "15.7.1",
|
||||
"@lokalise/node-api": "16.0.0",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.9",
|
||||
"@rsdoctor/rspack-plugin": "1.5.10",
|
||||
"@rspack/core": "2.0.2",
|
||||
"@rspack/dev-server": "2.0.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
@@ -186,7 +186,7 @@
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "29.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "17.0.2",
|
||||
"lint-staged": "17.0.3",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.18.1",
|
||||
@@ -197,7 +197,7 @@
|
||||
"serve": "14.2.6",
|
||||
"sinon": "22.0.0",
|
||||
"tar": "7.5.15",
|
||||
"terser-webpack-plugin": "5.5.0",
|
||||
"terser-webpack-plugin": "5.6.0",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.59.2",
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
import {
|
||||
mdiBattery,
|
||||
mdiBattery10,
|
||||
mdiBattery20,
|
||||
mdiBattery30,
|
||||
mdiBattery40,
|
||||
mdiBattery50,
|
||||
mdiBattery60,
|
||||
mdiBattery70,
|
||||
mdiBattery80,
|
||||
mdiBattery90,
|
||||
mdiBatteryAlertVariantOutline,
|
||||
mdiBatteryUnknown,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
const BATTERY_ICONS = {
|
||||
@@ -12,6 +26,18 @@ const BATTERY_ICONS = {
|
||||
90: "mdi:battery-90",
|
||||
100: "mdi:battery",
|
||||
};
|
||||
const BATTERY_ICON_PATHS = {
|
||||
10: mdiBattery10,
|
||||
20: mdiBattery20,
|
||||
30: mdiBattery30,
|
||||
40: mdiBattery40,
|
||||
50: mdiBattery50,
|
||||
60: mdiBattery60,
|
||||
70: mdiBattery70,
|
||||
80: mdiBattery80,
|
||||
90: mdiBattery90,
|
||||
100: mdiBattery,
|
||||
};
|
||||
const BATTERY_CHARGING_ICONS = {
|
||||
10: "mdi:battery-charging-10",
|
||||
20: "mdi:battery-charging-20",
|
||||
@@ -57,3 +83,15 @@ export const batteryLevelIcon = (
|
||||
}
|
||||
return BATTERY_ICONS[batteryRound];
|
||||
};
|
||||
|
||||
export const batteryLevelIconPath = (batteryLevel: number | string): string => {
|
||||
const batteryValue = Number(batteryLevel);
|
||||
if (isNaN(batteryValue)) {
|
||||
return mdiBatteryUnknown;
|
||||
}
|
||||
if (batteryValue <= 5) {
|
||||
return mdiBatteryAlertVariantOutline;
|
||||
}
|
||||
const batteryRound = Math.round(batteryValue / 10) * 10;
|
||||
return BATTERY_ICON_PATHS[batteryRound];
|
||||
};
|
||||
|
||||
@@ -137,7 +137,10 @@ export const computeEntityPickerDisplay = (
|
||||
hass.floors
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(hass);
|
||||
const isRTL = computeRTL(
|
||||
hass.language,
|
||||
hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || stateObj.entity_id;
|
||||
const secondary =
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import type { LitElement } from "lit";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, Translation } from "../../types";
|
||||
|
||||
export function computeRTL(hass: HomeAssistant) {
|
||||
const lang = hass.language || "en";
|
||||
if (hass.translationMetadata.translations[lang]) {
|
||||
return hass.translationMetadata.translations[lang].isRTL || false;
|
||||
export function computeRTL(
|
||||
language = "en",
|
||||
translations: Record<string, Translation>
|
||||
) {
|
||||
if (translations[language]) {
|
||||
return translations[language].isRTL || false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function computeRTLDirection(hass: HomeAssistant) {
|
||||
return emitRTLDirection(computeRTL(hass));
|
||||
return emitRTLDirection(
|
||||
computeRTL(hass.language, hass.translationMetadata.translations)
|
||||
);
|
||||
}
|
||||
|
||||
export function emitRTLDirection(rtl: boolean) {
|
||||
|
||||
@@ -293,7 +293,10 @@ export class StateHistoryChartLine extends LitElement {
|
||||
(changedProps.has("hass") &&
|
||||
this._hasEntityStatesChanged(changedProps.get("hass")))
|
||||
) {
|
||||
const rtl = computeRTL(this.hass);
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
let minYAxis: number | ((values: { min: number }) => number) | undefined =
|
||||
this.minYAxis;
|
||||
let maxYAxis: number | ((values: { max: number }) => number) | undefined =
|
||||
|
||||
@@ -144,7 +144,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
"ui.components.history_charts.duration"
|
||||
)}: ${millisecondsToDuration(durationInMs)}`;
|
||||
|
||||
const markerLocalized = !computeRTL(this.hass)
|
||||
const markerLocalized = !computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? marker
|
||||
: `<span style="direction: rtl;display:inline-block;margin-right:4px;margin-inline-end:4px;border-radius:10px;width:10px;height:10px;background-color:${color};"></span>`;
|
||||
|
||||
@@ -167,11 +170,12 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
changedProps.has("data") ||
|
||||
this._chartTime <
|
||||
new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES)
|
||||
this.isConnected &&
|
||||
(changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
changedProps.has("data") ||
|
||||
this._chartTime <
|
||||
new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES))
|
||||
) {
|
||||
// If the line is more than 5 minutes old, re-gen it
|
||||
// so the X axis grows even if there is no new data
|
||||
@@ -198,7 +202,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
? Math.max(this.paddingYAxis, this._yWidth)
|
||||
: 0;
|
||||
const labelMargin = 5;
|
||||
const rtl = computeRTL(this.hass);
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
this._chartOptions = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
|
||||
@@ -13,7 +13,9 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
import { formatDate } from "../../common/datetime/format_date";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import { formatTimeWithSeconds } from "../../common/datetime/format_time";
|
||||
import {
|
||||
formatNumber,
|
||||
getNumberFormatOptions,
|
||||
@@ -241,6 +243,8 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
private _renderTooltip = (params: any) => {
|
||||
const rendered: Record<string, boolean> = {};
|
||||
const chartIsBar = this.chartType.startsWith("bar");
|
||||
const period = this.period;
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
@@ -252,8 +256,67 @@ export class StatisticsChart extends LitElement {
|
||||
const statisticId = this._statisticIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
const entry = this.hass.entities[statisticId];
|
||||
// max series can have 3 values, as the second value is the max-min to form a band
|
||||
const rawValue = String(param.value[2] ?? param.value[1]);
|
||||
let rawValue: string;
|
||||
let rawTime: string;
|
||||
if (chartIsBar) {
|
||||
// For bar charts value is always second value.
|
||||
rawValue = String(param.value[1]);
|
||||
// Time value is third value (un-shifted date) if given, otherwise first value
|
||||
let startTime: Date;
|
||||
let endTime: Date | undefined;
|
||||
if (param.value[2]) {
|
||||
startTime = new Date(param.value[2]);
|
||||
if (param.value[3]) {
|
||||
endTime = new Date(param.value[3]);
|
||||
}
|
||||
} else {
|
||||
startTime = new Date(param.value[0]);
|
||||
}
|
||||
if (
|
||||
period === "year" ||
|
||||
period === "month" ||
|
||||
period === "week" ||
|
||||
period === "day"
|
||||
) {
|
||||
// For year/month/day periods, show only the date
|
||||
rawTime =
|
||||
formatDate(startTime, this.hass.locale, this.hass.config) +
|
||||
(endTime && period !== "day"
|
||||
? ` – ${formatDate(
|
||||
endTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}`
|
||||
: "") +
|
||||
"<br>";
|
||||
} else {
|
||||
// For other time periods, include time in render, and optionally show range
|
||||
// if we have an end time.
|
||||
rawTime =
|
||||
formatDateTimeWithSeconds(
|
||||
startTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) +
|
||||
(endTime
|
||||
? ` – ${formatTimeWithSeconds(
|
||||
endTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}`
|
||||
: "") +
|
||||
"<br>";
|
||||
}
|
||||
} else {
|
||||
// For lines max series can have 3 values, as the second value is the max-min to form a band
|
||||
rawValue = String(param.value[2] ?? param.value[1]);
|
||||
// Time value is always first value
|
||||
rawTime = `${formatDateTimeWithSeconds(
|
||||
new Date(param.value[0]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)} <br>`;
|
||||
}
|
||||
|
||||
const options = getNumberFormatOptions(stateObj, entry) ?? {
|
||||
maximumFractionDigits: 2,
|
||||
@@ -265,14 +328,7 @@ export class StatisticsChart extends LitElement {
|
||||
options
|
||||
)}${unit}`;
|
||||
|
||||
const time =
|
||||
index === 0
|
||||
? formatDateTimeWithSeconds(
|
||||
new Date(param.value[0]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) + "<br>"
|
||||
: "";
|
||||
const time = index === 0 ? rawTime : "";
|
||||
return `${time}${param.marker} ${param.seriesName}: ${value}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
@@ -368,7 +424,12 @@ export class StatisticsChart extends LitElement {
|
||||
nameTextStyle: {
|
||||
align: "left",
|
||||
},
|
||||
position: computeRTL(this.hass) ? "right" : "left",
|
||||
position: computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? "right"
|
||||
: "left",
|
||||
scale:
|
||||
this.chartType.startsWith("line") ||
|
||||
this.logarithmicScale ||
|
||||
@@ -506,33 +567,53 @@ export class StatisticsChart extends LitElement {
|
||||
const statDataSets: (LineSeriesOption | BarSeriesOption)[] = [];
|
||||
const statLegendData: typeof legendData = [];
|
||||
|
||||
// Place bars at centre of their specified time range if this is a bar chart
|
||||
// and the period is 5minute or hour.
|
||||
const centerBars =
|
||||
chartType === "bar" &&
|
||||
(this.period === "5minute" || this.period === "hour");
|
||||
|
||||
const pushData = (
|
||||
start: Date,
|
||||
end: Date,
|
||||
start: Date, // Data point start time
|
||||
end: Date, // Data point end time
|
||||
limit: Date, // Limit for end time (e.g. now)
|
||||
dataValues: (number | null)[][]
|
||||
) => {
|
||||
if (!dataValues.length) return;
|
||||
if (start > end) {
|
||||
// Limit for time range is lesser of overall limit and data point end
|
||||
limit = end.getTime() < limit.getTime() ? end : limit;
|
||||
if (start.getTime() > limit.getTime()) {
|
||||
// Drop data points that are after the requested endTime. This could happen if
|
||||
// endTime is "now" and client time is not in sync with server time.
|
||||
return;
|
||||
}
|
||||
statDataSets.forEach((d, i) => {
|
||||
if (
|
||||
chartType === "line" &&
|
||||
prevEndTime &&
|
||||
prevValues &&
|
||||
prevEndTime.getTime() !== start.getTime()
|
||||
) {
|
||||
// if the end of the previous data doesn't match the start of the current data,
|
||||
// we have to draw a gap so add a value at the end time, and then an empty value.
|
||||
d.data!.push([prevEndTime, ...prevValues[i]!]);
|
||||
d.data!.push([prevEndTime, null]);
|
||||
if (chartType === "line") {
|
||||
if (
|
||||
prevEndTime &&
|
||||
prevValues &&
|
||||
prevEndTime.getTime() !== start.getTime()
|
||||
) {
|
||||
// if the end of the previous data doesn't match the start of the current data,
|
||||
// we have to draw a gap so add a value at the end time, and then an empty value.
|
||||
d.data!.push([prevEndTime, ...prevValues[i]!]);
|
||||
d.data!.push([prevEndTime, null]);
|
||||
}
|
||||
d.data!.push([start, ...dataValues[i]!]);
|
||||
} else {
|
||||
let time = start;
|
||||
if (centerBars) {
|
||||
// If centering bars, set the time to the midpoint between start and end instead
|
||||
// of the start time.
|
||||
time = new Date((start.getTime() + end.getTime()) / 2);
|
||||
}
|
||||
// Data value should always be a scalar for bar charts. Pass in
|
||||
// real start time as extra value to allow formatting tooltip.
|
||||
d.data!.push([time, dataValues[i][0]!, start, end]);
|
||||
}
|
||||
d.data!.push([start, ...dataValues[i]!]);
|
||||
});
|
||||
prevValues = dataValues;
|
||||
prevEndTime = end;
|
||||
prevEndTime = limit;
|
||||
};
|
||||
|
||||
let color = colors[statistic_id];
|
||||
@@ -692,11 +773,7 @@ export class StatisticsChart extends LitElement {
|
||||
dataValues.push(val);
|
||||
});
|
||||
if (!this._hiddenStats.has(statistic_id)) {
|
||||
pushData(
|
||||
startDate,
|
||||
endDate.getTime() < endTime.getTime() ? endDate : endTime,
|
||||
dataValues
|
||||
);
|
||||
pushData(startDate, endDate, endTime, dataValues);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,14 @@ const isOn = (stateObj?: HassEntity) =>
|
||||
!STATES_OFF.includes(stateObj.state) &&
|
||||
!isUnavailableState(stateObj.state);
|
||||
|
||||
/**
|
||||
* @element ha-entity-toggle
|
||||
*
|
||||
* @cssprop --ha-entity-toggle-switch-width - Width of the switch track. Defaults to `38px`.
|
||||
* @cssprop --ha-entity-toggle-switch-size - Height of the switch track. Defaults to `20px`.
|
||||
* @cssprop --ha-entity-toggle-switch-thumb-size - Size of the switch thumb. Defaults to `14px`.
|
||||
*/
|
||||
|
||||
@customElement("ha-entity-toggle")
|
||||
export class HaEntityToggle extends LitElement {
|
||||
// hass is not a property so that we only re-render on stateObj changes
|
||||
@@ -165,9 +173,9 @@ export class HaEntityToggle extends LitElement {
|
||||
white-space: nowrap;
|
||||
}
|
||||
ha-switch {
|
||||
--ha-switch-width: 38px;
|
||||
--ha-switch-size: 20px;
|
||||
--ha-switch-thumb-size: 14px;
|
||||
--ha-switch-width: var(--ha-entity-toggle-switch-width, 38px);
|
||||
--ha-switch-size: var(--ha-entity-toggle-switch-size, 20px);
|
||||
--ha-switch-thumb-size: var(--ha-entity-toggle-switch-thumb-size, 14px);
|
||||
}
|
||||
ha-icon-button {
|
||||
--ha-icon-button-size: 40px;
|
||||
|
||||
@@ -130,7 +130,6 @@ export class HaStateLabelBadge extends LitElement {
|
||||
? html`<ha-state-icon
|
||||
.icon=${this.icon}
|
||||
.stateObj=${entityState}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>`
|
||||
: ""}
|
||||
${value && !image && !showIcon
|
||||
|
||||
@@ -210,7 +210,10 @@ export class HaStatisticPicker extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(hass);
|
||||
const isRTL = computeRTL(
|
||||
hass.language,
|
||||
hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const output: StatisticComboBoxItem[] = [];
|
||||
|
||||
@@ -353,7 +356,10 @@ export class HaStatisticPicker extends LitElement {
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || statisticId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
|
||||
@@ -98,7 +98,6 @@ export class StateBadge extends LitElement {
|
||||
const domain = stateObj ? computeStateDomain(stateObj) : undefined;
|
||||
|
||||
return html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
style=${styleMap(this._iconStyle)}
|
||||
data-domain=${ifDefined(domain)}
|
||||
data-state=${ifDefined(stateObj?.state)}
|
||||
|
||||
@@ -184,7 +184,10 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
const allEntityIds = Object.values(controlEntities).flat();
|
||||
const uniqueEntityIds = Array.from(new Set(allEntityIds));
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
uniqueEntityIds.forEach((entityId) => {
|
||||
if (isSelected(entityId)) {
|
||||
@@ -261,7 +264,6 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
${item.type === "entity" && item.stateObj
|
||||
? html`<ha-state-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${item.stateObj}
|
||||
></ha-state-icon>`
|
||||
: item.domain
|
||||
|
||||
@@ -39,7 +39,12 @@ export class HaEntitiesDisplayEditor extends LitElement {
|
||||
const items: DisplayItem[] = entities.map((entity) => ({
|
||||
value: entity.entity_id,
|
||||
label: computeStateName(entity),
|
||||
icon: entityIcon(this.hass, entity),
|
||||
icon: entityIcon(
|
||||
this.hass.entities,
|
||||
this.hass.config,
|
||||
this.hass.connection,
|
||||
entity
|
||||
),
|
||||
}));
|
||||
|
||||
const value: DisplayValue = {
|
||||
|
||||
@@ -122,11 +122,7 @@ export class HaFilterEntities extends LitElement {
|
||||
.selected=${this.value?.includes(entity.entity_id) ?? false}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entity}
|
||||
></ha-state-icon>
|
||||
<ha-state-icon slot="graphic" .stateObj=${entity}></ha-state-icon>
|
||||
${computeStateName(entity)}
|
||||
</ha-check-list-item>`;
|
||||
|
||||
|
||||
@@ -137,7 +137,10 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
.selected=${this.value?.areas?.includes(area.area_id) || false}
|
||||
.type=${"areas"}
|
||||
class=${classMap({
|
||||
rtl: computeRTL(this.hass),
|
||||
rtl: computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
),
|
||||
floor: hasFloor,
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -166,7 +166,6 @@ export class HaRelatedItems extends LitElement {
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entity}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
@@ -322,7 +321,6 @@ export class HaRelatedItems extends LitElement {
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${group}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
@@ -347,7 +345,6 @@ export class HaRelatedItems extends LitElement {
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${scene}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
@@ -400,7 +397,6 @@ export class HaRelatedItems extends LitElement {
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${automation}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
@@ -452,7 +448,6 @@ export class HaRelatedItems extends LitElement {
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${script}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
|
||||
@@ -63,7 +63,12 @@ export class HaSelectBox extends LitElement {
|
||||
const selected = option.value === this.value;
|
||||
|
||||
const isDark = this.hass?.themes.darkMode || false;
|
||||
const isRTL = this.hass ? computeRTL(this.hass) : false;
|
||||
const isRTL = this.hass
|
||||
? computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
: false;
|
||||
|
||||
const imageSrc =
|
||||
typeof option.image === "object"
|
||||
|
||||
@@ -36,7 +36,15 @@ export class HaIconSelector extends LitElement {
|
||||
const placeholder =
|
||||
this.selector.icon?.placeholder ||
|
||||
stateObj?.attributes.icon ||
|
||||
(stateObj && until(entityIcon(this.hass, stateObj)));
|
||||
(stateObj &&
|
||||
until(
|
||||
entityIcon(
|
||||
this.hass.entities,
|
||||
this.hass.config,
|
||||
this.hass.connection,
|
||||
stateObj
|
||||
)
|
||||
));
|
||||
|
||||
return html`
|
||||
<ha-icon-picker
|
||||
@@ -51,11 +59,7 @@ export class HaIconSelector extends LitElement {
|
||||
>
|
||||
${!placeholder && stateObj
|
||||
? html`
|
||||
<ha-state-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-icon>
|
||||
<ha-state-icon slot="start" .stateObj=${stateObj}></ha-state-icon>
|
||||
`
|
||||
: nothing}
|
||||
</ha-icon-picker>
|
||||
|
||||
@@ -523,7 +523,10 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
}
|
||||
|
||||
private _renderUserItem(selectedPanel: string) {
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
const isSelected = selectedPanel === "profile";
|
||||
|
||||
return html`
|
||||
@@ -561,9 +564,9 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
id="sidebar-external-config"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("ui.sidebar.external_app_configuration")}</span
|
||||
>
|
||||
<span class="item-text" slot="headline">
|
||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</ha-list-item-button>
|
||||
${!this.alwaysExpand
|
||||
? this._renderToolTip(
|
||||
@@ -740,6 +743,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
--ha-row-item-min-height: var(--ha-space-10);
|
||||
--ha-row-item-padding-block: 0;
|
||||
--ha-row-item-padding-inline: var(--ha-space-3);
|
||||
width: var(--ha-space-12);
|
||||
position: relative;
|
||||
transition: width var(--ha-animation-duration-normal) ease;
|
||||
@@ -840,21 +844,12 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
}
|
||||
|
||||
ha-user-badge {
|
||||
width: var(--ha-space-10);
|
||||
height: var(--ha-space-10);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
ha-list-item-button.user {
|
||||
--ha-row-item-padding-inline: var(--ha-space-2) var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-list-item-button.user.rtl {
|
||||
--ha-row-item-padding-inline: var(--ha-space-4) var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-user-badge {
|
||||
flex-shrink: 0;
|
||||
margin-right: calc(var(--ha-space-2) * -1);
|
||||
--ha-row-item-padding-inline: var(--ha-space-1) 0;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
|
||||
@@ -1,31 +1,46 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import {
|
||||
configContext,
|
||||
connectionContext,
|
||||
entitiesContext,
|
||||
} from "../data/context";
|
||||
import {
|
||||
DEFAULT_DOMAIN_ICON,
|
||||
entityIcon,
|
||||
FALLBACK_DOMAIN_ICONS,
|
||||
} from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-state-icon")
|
||||
export class HaStateIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@property({ attribute: false }) public stateValue?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
protected _config?: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
protected _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
protected _entities?: ContextType<typeof entitiesContext>;
|
||||
|
||||
protected render() {
|
||||
const overrideIcon =
|
||||
this.icon ||
|
||||
(this.stateObj && this.hass?.entities[this.stateObj.entity_id]?.icon) ||
|
||||
(this.stateObj && this._entities?.[this.stateObj.entity_id]?.icon) ||
|
||||
this.stateObj?.attributes.icon;
|
||||
if (overrideIcon) {
|
||||
return html`<ha-icon .icon=${overrideIcon}></ha-icon>`;
|
||||
@@ -33,17 +48,21 @@ export class HaStateIcon extends LitElement {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
if (!this.hass) {
|
||||
if (!this._config || !this._connection || !this._entities) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
const icon = entityIcon(this.hass, this.stateObj, this.stateValue).then(
|
||||
(icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
const icon = entityIcon(
|
||||
this._entities,
|
||||
this._config.config,
|
||||
this._connection.connection,
|
||||
this.stateObj,
|
||||
this.stateValue
|
||||
).then((icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
);
|
||||
return this._renderFallback();
|
||||
});
|
||||
return html`${until(icon)}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1136,7 +1136,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
let rtl = false;
|
||||
let showEntityId = false;
|
||||
if (type === "area" || type === "floor") {
|
||||
rtl = computeRTL(this.hass);
|
||||
rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
hasFloor =
|
||||
type === "area" && !!(item as FloorComboBoxItem).area?.floor_id;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
|
||||
/**
|
||||
* @element ha-row-item
|
||||
@@ -46,13 +46,34 @@ export class HaRowItem extends LitElement {
|
||||
|
||||
protected readonly _slotController = new HasSlotController(
|
||||
this,
|
||||
"start",
|
||||
"end",
|
||||
"headline",
|
||||
"supporting-text",
|
||||
"content"
|
||||
);
|
||||
|
||||
@state() private _hasStart = false;
|
||||
|
||||
@state() private _hasEnd = false;
|
||||
|
||||
private _onSlotChange(name: "start" | "end") {
|
||||
return (ev: Event) => {
|
||||
const slot = ev.target as HTMLSlotElement;
|
||||
const hasContent = slot
|
||||
.assignedNodes({ flatten: true })
|
||||
.some(
|
||||
(node) =>
|
||||
node.nodeType === Node.ELEMENT_NODE ||
|
||||
(node.nodeType === Node.TEXT_NODE &&
|
||||
(node as Text).textContent?.trim() !== "")
|
||||
);
|
||||
if (name === "start") {
|
||||
this._hasStart = hasContent;
|
||||
} else {
|
||||
this._hasEnd = hasContent;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return this._renderBase(this._renderInner());
|
||||
}
|
||||
@@ -65,16 +86,16 @@ export class HaRowItem extends LitElement {
|
||||
const hasContent = this._slotController.test("content");
|
||||
|
||||
return html`
|
||||
<div part="start" class="start">
|
||||
<slot name="start"></slot>
|
||||
<div part="start" class="start" ?hidden=${!this._hasStart}>
|
||||
<slot name="start" @slotchange=${this._onSlotChange("start")}></slot>
|
||||
</div>
|
||||
<div part="content" class="content">
|
||||
${hasContent
|
||||
? html`<slot name="content"></slot>`
|
||||
: this._renderDefaultContent()}
|
||||
</div>
|
||||
<div part="end" class="end">
|
||||
<slot name="end"></slot>
|
||||
<div part="end" class="end" ?hidden=${!this._hasEnd}>
|
||||
<slot name="end" @slotchange=${this._onSlotChange("end")}></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -142,8 +163,8 @@ export class HaRowItem extends LitElement {
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
:host(:not(:has([slot="start"]))) .start,
|
||||
:host(:not(:has([slot="end"]))) .end {
|
||||
.start[hidden],
|
||||
.end[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.headline {
|
||||
|
||||
@@ -37,7 +37,6 @@ class HaEntityMarker extends LitElement {
|
||||
></div>`
|
||||
: this.showIcon && this.entityId
|
||||
? html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.hass?.states[this.entityId]}
|
||||
></ha-state-icon>`
|
||||
: !this.entityUnit
|
||||
|
||||
@@ -59,7 +59,10 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
icon = mdiSpeakerPause;
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const { primary, secondary } = this._computeDisplayData(
|
||||
this.entityId,
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker";
|
||||
import "../../ha-dialog";
|
||||
import "../../ha-adaptive-dialog";
|
||||
import "../../ha-dialog-header";
|
||||
import "../../ha-icon-button";
|
||||
import "../../ha-icon-next";
|
||||
@@ -153,7 +153,7 @@ class DialogTargetDetails extends LitElement implements HassDialog {
|
||||
!this._entitySourcesLoaded;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
<ha-adaptive-dialog
|
||||
.open=${this._opened}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.components.target-picker.target_details"
|
||||
@@ -187,7 +187,7 @@ class DialogTargetDetails extends LitElement implements HassDialog {
|
||||
`}
|
||||
</ha-list-base>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
</ha-adaptive-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,6 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
: this.type === "entity"
|
||||
? html`
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObject ||
|
||||
({
|
||||
entity_id: this.itemId,
|
||||
@@ -224,7 +223,10 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
: this.subEntry && this.type === "entity"
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${computeRTL(this.hass)
|
||||
.path=${computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? mdiChevronLeft
|
||||
: mdiChevronRight}
|
||||
slot="end"
|
||||
@@ -613,7 +615,14 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
const context = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(computeRTL(this.hass) ? " ◂ " : " ▸ ");
|
||||
.join(
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? " ◂ "
|
||||
: " ▸ "
|
||||
);
|
||||
return {
|
||||
name: entityName || deviceName || item,
|
||||
context,
|
||||
|
||||
@@ -76,7 +76,6 @@ export class HaTargetPickerValueChip extends LitElement {
|
||||
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
|
||||
: stateObject
|
||||
? html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObject}
|
||||
></ha-state-icon>`
|
||||
: nothing}
|
||||
|
||||
@@ -99,7 +99,8 @@ export class HaTileContainer extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
padding: 0 10px;
|
||||
min-height: var(--row-height, 56px);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import type { Trigger } from "../../data/automation";
|
||||
import { migrateAutomationTrigger } from "../../data/automation";
|
||||
@@ -23,9 +22,10 @@ import "../../panels/logbook/ha-logbook-renderer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-code-editor";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-tab-group";
|
||||
import "../ha-tab-group-tab";
|
||||
import "./hat-logbook-note";
|
||||
import type { NodeInfo } from "./hat-script-graph";
|
||||
import { traceTabStyles } from "./trace-tab-styles";
|
||||
|
||||
const TRACE_PATH_TABS = [
|
||||
"step_config",
|
||||
@@ -66,21 +66,21 @@ export class HaTracePathDetails extends LitElement {
|
||||
${this._renderSelectedTraceInfo()}
|
||||
</div>
|
||||
|
||||
<div class="tabs top">
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${TRACE_PATH_TABS.map(
|
||||
(view) => html`
|
||||
<button
|
||||
.view=${view}
|
||||
class=${classMap({ active: this._view === view })}
|
||||
@click=${this._showTab}
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === view}
|
||||
.panel=${view}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.automation.trace.tabs.${view}`
|
||||
)}
|
||||
</button>
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-tab-group>
|
||||
${this._view === "step_config"
|
||||
? this._renderSelectedConfig()
|
||||
: this._view === "changed_variables"
|
||||
@@ -308,7 +308,12 @@ export class HaTracePathDetails extends LitElement {
|
||||
? this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.path.no_variables_changed"
|
||||
)
|
||||
: html`<pre>${dump(trace.changed_variables).trimEnd()}</pre>`}
|
||||
: html`<ha-code-editor
|
||||
read-only
|
||||
dir="ltr"
|
||||
.hass=${this.hass}
|
||||
.value=${dump(trace.changed_variables).trimEnd()}
|
||||
></ha-code-editor>`}
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
@@ -383,13 +388,12 @@ export class HaTracePathDetails extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _showTab(ev) {
|
||||
this._view = ev.target.view;
|
||||
private _handleTabChanged(ev: CustomEvent) {
|
||||
this._view = ev.detail.name as typeof this._view;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
traceTabStyles,
|
||||
css`
|
||||
.padded-box {
|
||||
margin: 16px;
|
||||
@@ -406,6 +410,16 @@ export class HaTracePathDetails extends LitElement {
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
ha-tab-group {
|
||||
background-color: var(--primary-background-color);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
ha-tab-group-tab::part(base) {
|
||||
padding: 2px 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { css } from "lit";
|
||||
|
||||
export const traceTabStyles = css`
|
||||
.tabs {
|
||||
background-color: var(--primary-background-color);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
display: flex;
|
||||
padding-left: 4px;
|
||||
padding-inline-start: 4px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
|
||||
.tabs.top {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.tabs > * {
|
||||
padding: 2px 16px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
bottom: -1px;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
user-select: none;
|
||||
background: none;
|
||||
color: var(--primary-text-color);
|
||||
outline: none;
|
||||
transition: background 15ms linear;
|
||||
}
|
||||
|
||||
.tabs > *.active {
|
||||
border-bottom-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.tabs > *:focus,
|
||||
.tabs > *:hover {
|
||||
background: var(--secondary-background-color);
|
||||
}
|
||||
`;
|
||||
@@ -164,6 +164,7 @@ export interface BatterySourceTypeEnergyPreference {
|
||||
stat_energy_to: string;
|
||||
stat_rate?: string; // always available if power_config is set
|
||||
power_config?: PowerConfig;
|
||||
stat_soc?: string;
|
||||
}
|
||||
export interface GasSourceTypeEnergyPreference {
|
||||
type: "gas";
|
||||
|
||||
@@ -96,7 +96,10 @@ export const getEntities = (
|
||||
|
||||
const domainName = domainToName(hass.localize, computeDomain(entityId));
|
||||
|
||||
const isRTL = computeRTL(hass);
|
||||
const isRTL = computeRTL(
|
||||
hass.language,
|
||||
hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
|
||||
+26
-9
@@ -456,11 +456,13 @@ const getIconFromTranslations = (
|
||||
};
|
||||
|
||||
export const entityIcon = async (
|
||||
hass: HomeAssistant,
|
||||
entities: HomeAssistant["entities"],
|
||||
hassConfig: HomeAssistant["config"],
|
||||
hassConnection: Connection,
|
||||
stateObj: HassEntity,
|
||||
state?: string
|
||||
) => {
|
||||
const entry = hass.entities?.[stateObj.entity_id] as
|
||||
const entry = entities?.[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
if (entry?.icon) {
|
||||
@@ -468,7 +470,14 @@ export const entityIcon = async (
|
||||
}
|
||||
const domain = computeStateDomain(stateObj);
|
||||
|
||||
return getEntityIcon(hass, domain, stateObj, state, entry);
|
||||
return getEntityIcon(
|
||||
hassConfig,
|
||||
hassConnection,
|
||||
domain,
|
||||
stateObj,
|
||||
state,
|
||||
entry
|
||||
);
|
||||
};
|
||||
|
||||
export const entryIcon = async (
|
||||
@@ -480,11 +489,19 @@ export const entryIcon = async (
|
||||
}
|
||||
const stateObj = hass.states[entry.entity_id] as HassEntity | undefined;
|
||||
const domain = computeDomain(entry.entity_id);
|
||||
return getEntityIcon(hass, domain, stateObj, undefined, entry);
|
||||
return getEntityIcon(
|
||||
hass.config,
|
||||
hass.connection,
|
||||
domain,
|
||||
stateObj,
|
||||
undefined,
|
||||
entry
|
||||
);
|
||||
};
|
||||
|
||||
const getEntityIcon = async (
|
||||
hass: HomeAssistant,
|
||||
hassConfig: HomeAssistant["config"],
|
||||
hassConnection: Connection,
|
||||
domain: string,
|
||||
stateObj?: HassEntity,
|
||||
stateValue?: string,
|
||||
@@ -498,8 +515,8 @@ const getEntityIcon = async (
|
||||
let icon: string | undefined;
|
||||
if (translation_key && platform) {
|
||||
const platformIcons = await getPlatformIcons(
|
||||
hass.config,
|
||||
hass.connection,
|
||||
hassConfig,
|
||||
hassConnection,
|
||||
platform
|
||||
);
|
||||
if (platformIcons) {
|
||||
@@ -515,8 +532,8 @@ const getEntityIcon = async (
|
||||
|
||||
if (!icon) {
|
||||
const entityComponentIcons = await getComponentIcons(
|
||||
hass.connection,
|
||||
hass.config,
|
||||
hassConnection,
|
||||
hassConfig,
|
||||
domain
|
||||
);
|
||||
if (entityComponentIcons) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import type { LovelaceCardConfig } from "./card";
|
||||
|
||||
export interface ToggleActionConfig extends BaseActionConfig {
|
||||
action: "toggle";
|
||||
@@ -38,13 +37,6 @@ export interface AssistActionConfig extends BaseActionConfig {
|
||||
start_listening?: boolean;
|
||||
}
|
||||
|
||||
export interface ShowPopupActionConfig extends BaseActionConfig {
|
||||
action: "show-popup";
|
||||
desktop_mode?: "popover" | "dialog";
|
||||
mobile_mode?: "bottom-sheet" | "dialog";
|
||||
cards: LovelaceCardConfig[];
|
||||
}
|
||||
|
||||
export interface NoActionConfig extends BaseActionConfig {
|
||||
action: "none";
|
||||
}
|
||||
@@ -77,6 +69,5 @@ export type ActionConfig =
|
||||
| UrlActionConfig
|
||||
| MoreInfoActionConfig
|
||||
| AssistActionConfig
|
||||
| ShowPopupActionConfig
|
||||
| NoActionConfig
|
||||
| CustomActionConfig;
|
||||
|
||||
@@ -18,7 +18,7 @@ import "../../../components/ha-slider";
|
||||
import "../../../components/ha-time-input";
|
||||
import "../../../components/input/ha-input";
|
||||
import { isTiltOnly } from "../../../data/cover";
|
||||
import { isUnavailableState } from "../../../data/entity/entity";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { ImageEntity } from "../../../data/image";
|
||||
import { computeImageUrl } from "../../../data/image";
|
||||
import "../../../panels/lovelace/components/hui-timestamp-display";
|
||||
@@ -266,7 +266,7 @@ class EntityPreviewRow extends LitElement {
|
||||
<div class="numberflex">
|
||||
<ha-slider
|
||||
labeled
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
.step=${Number(stateObj.attributes.step)}
|
||||
.min=${Number(stateObj.attributes.min)}
|
||||
.max=${Number(stateObj.attributes.max)}
|
||||
@@ -280,7 +280,7 @@ class EntityPreviewRow extends LitElement {
|
||||
: html`<div class="numberflex numberstate">
|
||||
<ha-input
|
||||
auto-validate
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
pattern="[0-9]+([\\.][0-9]+)?"
|
||||
.step=${Number(stateObj.attributes.step)}
|
||||
.min=${Number(stateObj.attributes.min)}
|
||||
@@ -303,7 +303,7 @@ class EntityPreviewRow extends LitElement {
|
||||
<ha-select
|
||||
.label=${computeStateName(stateObj)}
|
||||
.value=${stateObj.state}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
.options=${stateObj.attributes.options?.map((option) => ({
|
||||
value: option,
|
||||
label: this.hass!.formatEntityState(stateObj, option),
|
||||
|
||||
@@ -46,8 +46,7 @@ class MoreInfoAlarmControlPanel extends LitElement {
|
||||
? html`
|
||||
<div class="status">
|
||||
<div class="icon">
|
||||
<ha-state-icon .hass=${this.hass} .stateObj=${this.stateObj}>
|
||||
</ha-state-icon>
|
||||
<ha-state-icon .stateObj=${this.stateObj}> </ha-state-icon>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-date-input";
|
||||
import "../../../components/ha-time-input";
|
||||
import { isUnavailableState, UNKNOWN } from "../../../data/entity/entity";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
|
||||
import {
|
||||
setInputDateTimeValue,
|
||||
stateToIsoDateString,
|
||||
@@ -27,7 +27,7 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
<ha-date-input
|
||||
.locale=${this.hass.locale}
|
||||
.value=${stateToIsoDateString(this.stateObj)}
|
||||
.disabled=${isUnavailableState(this.stateObj.state)}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@value-changed=${this._dateChanged}
|
||||
>
|
||||
</ha-date-input>
|
||||
@@ -42,7 +42,7 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
? this.stateObj.state.split(" ")[1]
|
||||
: this.stateObj.state}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${isUnavailableState(this.stateObj.state)}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@value-changed=${this._timeChanged}
|
||||
@click=${this._stopEventPropagation}
|
||||
></ha-time-input>
|
||||
|
||||
@@ -97,10 +97,7 @@ class MoreInfoLock extends LitElement {
|
||||
<div class="status">
|
||||
<span></span>
|
||||
<div class="icon">
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-state-icon>
|
||||
<ha-state-icon .stateObj=${this.stateObj}></ha-state-icon>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -190,7 +190,6 @@ class MoreInfoWeather extends LitElement {
|
||||
<ha-state-icon
|
||||
class="weather-icon"
|
||||
.stateObj=${this.stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
`}
|
||||
</div>
|
||||
|
||||
@@ -594,7 +594,10 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
? !favoritesHandler.hasCustomFavorites(favoritesContext.entry)
|
||||
: false;
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-adaptive-dialog
|
||||
|
||||
@@ -24,8 +24,8 @@ import "../../components/ha-two-pane-top-app-bar-fixed";
|
||||
import type {
|
||||
Calendar,
|
||||
CalendarEvent,
|
||||
CalendarEventSubscription,
|
||||
CalendarEventApiData,
|
||||
CalendarEventSubscription,
|
||||
} from "../../data/calendar";
|
||||
import {
|
||||
getCalendars,
|
||||
@@ -144,7 +144,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
>
|
||||
<ha-state-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${selCal}
|
||||
style="--icon-primary-color: ${selCal.backgroundColor}"
|
||||
></ha-state-icon>
|
||||
|
||||
@@ -970,7 +970,14 @@ class DialogAddAutomationElement
|
||||
|
||||
subtitle = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(computeRTL(this.hass) ? " ◂ " : " ▸ ");
|
||||
.join(
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? " ◂ "
|
||||
: " ▸ "
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -769,7 +769,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
private _renderEntityIcon =
|
||||
(stateObj: HassEntity) => (slot: string | undefined) =>
|
||||
html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
slot=${ifDefined(slot)}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-icon>`;
|
||||
@@ -867,10 +866,13 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
undefined
|
||||
);
|
||||
|
||||
const filteredFloors = this._floorAreas.filter(
|
||||
({ id, areas }) => id !== undefined && areas.length
|
||||
);
|
||||
this._floorAreas.forEach((floor) => {
|
||||
this._entries[floor.id || `floor${TARGET_SEPARATOR}`] = {
|
||||
// auto expand if only one floor is present
|
||||
open: this._floorAreas.length === 1,
|
||||
open: filteredFloors.length === 1 && filteredFloors[0].id === floor.id,
|
||||
areas: {},
|
||||
};
|
||||
|
||||
|
||||
@@ -300,7 +300,10 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
let showEntityId = false;
|
||||
|
||||
if (type === "area" || type === "floor") {
|
||||
rtl = computeRTL(this.hass);
|
||||
rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
hasFloor =
|
||||
type === "area" && !!(item as FloorComboBoxItem).area?.floor_id;
|
||||
}
|
||||
|
||||
@@ -302,7 +302,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
showNarrow: true,
|
||||
template: (automation) =>
|
||||
html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${automation}
|
||||
style=${styleMap({
|
||||
color:
|
||||
|
||||
@@ -303,7 +303,9 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
private _updateSize(clientX: number) {
|
||||
let delta = this._resizeStartX - clientX;
|
||||
|
||||
if (computeRTL(this.hass)) {
|
||||
if (
|
||||
computeRTL(this.hass.language, this.hass.translationMetadata.translations)
|
||||
) {
|
||||
delta = -delta;
|
||||
}
|
||||
|
||||
@@ -350,14 +352,24 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
private _increaseSize = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
|
||||
this._resizeStartX -= computeRTL(this.hass) ? 10 : -10;
|
||||
this._resizeStartX -= computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? 10
|
||||
: -10;
|
||||
this._keyboardResize();
|
||||
};
|
||||
|
||||
private _decreaseSize = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
|
||||
this._resizeStartX += computeRTL(this.hass) ? 10 : -10;
|
||||
this._resizeStartX += computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? 10
|
||||
: -10;
|
||||
this._keyboardResize();
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
@@ -21,6 +20,8 @@ import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-tab-group";
|
||||
import "../../../components/ha-tab-group-tab";
|
||||
import "../../../components/trace/ha-trace-blueprint-config";
|
||||
import "../../../components/trace/ha-trace-config";
|
||||
import "../../../components/trace/ha-trace-logbook";
|
||||
@@ -31,7 +32,6 @@ import type {
|
||||
HatScriptGraph,
|
||||
NodeInfo,
|
||||
} from "../../../components/trace/hat-script-graph";
|
||||
import { traceTabStyles } from "../../../components/trace/trace-tab-styles";
|
||||
import type { AutomationEntity } from "../../../data/automation";
|
||||
import type { LogbookEntry } from "../../../data/logbook";
|
||||
import { getLogbookDataForContext } from "../../../data/logbook";
|
||||
@@ -172,7 +172,10 @@ export class HaAutomationTrace extends LitElement {
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.older_trace"
|
||||
)}
|
||||
.path=${computeRTL(this.hass!)
|
||||
.path=${computeRTL(
|
||||
this.hass!.language,
|
||||
this.hass!.translationMetadata.translations
|
||||
)
|
||||
? mdiRayStartArrow
|
||||
: mdiRayEndArrow}
|
||||
.disabled=${this._traces[this._traces.length - 1].run_id ===
|
||||
@@ -189,7 +192,10 @@ export class HaAutomationTrace extends LitElement {
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.newer_trace"
|
||||
)}
|
||||
.path=${computeRTL(this.hass!)
|
||||
.path=${computeRTL(
|
||||
this.hass!.language,
|
||||
this.hass!.translationMetadata.translations
|
||||
)
|
||||
? mdiRayEndArrow
|
||||
: mdiRayStartArrow}
|
||||
.disabled=${this._traces[0].run_id === this._runId}
|
||||
@@ -223,40 +229,34 @@ export class HaAutomationTrace extends LitElement {
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<div class="tabs top">
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${TABS.map(
|
||||
(view) => html`
|
||||
<button
|
||||
tabindex="0"
|
||||
.view=${view}
|
||||
class=${classMap({
|
||||
active: this._view === view,
|
||||
})}
|
||||
@click=${this._showTab}
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === view}
|
||||
.panel=${view}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.automation.trace.tabs.${view}`
|
||||
)}
|
||||
</button>
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
${this._trace.blueprint_inputs
|
||||
? html`
|
||||
<button
|
||||
tabindex="0"
|
||||
.view=${"blueprint"}
|
||||
class=${classMap({
|
||||
active: this._view === "blueprint",
|
||||
})}
|
||||
@click=${this._showTab}
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === "blueprint"}
|
||||
panel="blueprint"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.automation.trace.tabs.blueprint_config`
|
||||
)}
|
||||
</button>
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</ha-tab-group>
|
||||
${this._selected === undefined ||
|
||||
this._logbookEntries === undefined ||
|
||||
trackedNodes === undefined
|
||||
@@ -483,8 +483,8 @@ export class HaAutomationTrace extends LitElement {
|
||||
this._logbookEntries = traceInfo.logbookEntries;
|
||||
}
|
||||
|
||||
private _showTab(ev: Event) {
|
||||
this._view = (ev.target as any).view;
|
||||
private _handleTabChanged(ev: CustomEvent) {
|
||||
this._view = ev.detail.name as typeof this._view;
|
||||
}
|
||||
|
||||
private _timelinePathPicked(ev: CustomEvent) {
|
||||
@@ -536,7 +536,6 @@ export class HaAutomationTrace extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
traceTabStyles,
|
||||
css`
|
||||
.toolbar {
|
||||
display: flex;
|
||||
@@ -599,6 +598,14 @@ export class HaAutomationTrace extends LitElement {
|
||||
overflow-y: auto;
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
ha-tab-group {
|
||||
background-color: var(--primary-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-tab-group-tab::part(base) {
|
||||
padding: 2px 16px;
|
||||
}
|
||||
:host([narrow]) .info {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ export const getTargetIcon = (
|
||||
|
||||
if (targetType === "entity" && hass.states[targetId]) {
|
||||
return html`<ha-state-icon
|
||||
.hass=${hass}
|
||||
.stateObj=${hass.states[targetId]}
|
||||
.slot=${slot}
|
||||
></ha-state-icon>`;
|
||||
|
||||
@@ -588,7 +588,10 @@ export class HaConfigDevicePage extends LitElement {
|
||||
</ha-list-item>
|
||||
<ha-tooltip
|
||||
.for="scene-${slugify(entityState.entity_id)}"
|
||||
placement=${computeRTL(this.hass)
|
||||
placement=${computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? "left"
|
||||
: "right"}
|
||||
>
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
import type { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy";
|
||||
|
||||
const energyUnitClasses = ["energy"];
|
||||
const socStatisticsUnits = ["%"];
|
||||
const socDeviceClass = "battery";
|
||||
|
||||
@customElement("dialog-energy-battery-settings")
|
||||
export class DialogEnergyBatterySettings
|
||||
@@ -179,6 +181,21 @@ export class DialogEnergyBatterySettings
|
||||
@power-config-changed=${this._handlePowerConfigChanged}
|
||||
></ha-energy-power-config>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
||||
.value=${this._source.stat_soc}
|
||||
.includeStatisticsUnitOfMeasurement=${socStatisticsUnits}
|
||||
.includeDeviceClass=${socDeviceClass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.state_of_charge"
|
||||
)}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.state_of_charge_helper"
|
||||
)}
|
||||
@value-changed=${this._statisticSocChanged}
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@@ -231,6 +248,13 @@ export class DialogEnergyBatterySettings
|
||||
this._powerConfig = ev.detail.powerConfig;
|
||||
}
|
||||
|
||||
private _statisticSocChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = {
|
||||
...this._source!,
|
||||
stat_soc: ev.detail.value || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
try {
|
||||
const source: BatterySourceTypeEnergyPreference = {
|
||||
@@ -244,6 +268,10 @@ export class DialogEnergyBatterySettings
|
||||
source.power_config = { ...this._powerConfig };
|
||||
}
|
||||
|
||||
if (this._source!.stat_soc) {
|
||||
source.stat_soc = this._source!.stat_soc;
|
||||
}
|
||||
|
||||
await this._params!.saveCallback(source);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
@@ -256,7 +284,8 @@ export class DialogEnergyBatterySettings
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-statistic-picker {
|
||||
ha-statistic-picker,
|
||||
ha-energy-power-config {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
|
||||
@@ -162,7 +162,12 @@ export class DialogVacuumSegmentMapping
|
||||
"ui.dialogs.vacuum_segment_mapping.title"
|
||||
)}
|
||||
.headerSubtitle=${breadcrumb.join(
|
||||
computeRTL(this.hass) ? " ◂ " : " ▸ "
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? " ◂ "
|
||||
: " ▸ "
|
||||
)}
|
||||
>
|
||||
<ha-vacuum-segment-area-mapper
|
||||
|
||||
@@ -419,7 +419,15 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
)}
|
||||
.placeholder=${this.entry.original_icon ||
|
||||
stateObj?.attributes.icon ||
|
||||
(stateObj && until(entityIcon(this.hass, stateObj))) ||
|
||||
(stateObj &&
|
||||
until(
|
||||
entityIcon(
|
||||
this.hass.entities,
|
||||
this.hass.config,
|
||||
this.hass.connection,
|
||||
stateObj
|
||||
)
|
||||
)) ||
|
||||
until(entryIcon(this.hass, this.entry))}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
@@ -427,7 +435,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
? html`
|
||||
<ha-state-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-icon>
|
||||
`
|
||||
|
||||
@@ -345,7 +345,6 @@ export class HaConfigEntities extends LitElement {
|
||||
: undefined
|
||||
)}
|
||||
slot="item-icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entry.entity}
|
||||
></ha-state-icon>
|
||||
`
|
||||
|
||||
@@ -335,10 +335,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
moveable: false,
|
||||
template: (helper) =>
|
||||
helper.entity
|
||||
? html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${helper.entity}
|
||||
></ha-state-icon>`
|
||||
? html`<ha-state-icon .stateObj=${helper.entity}></ha-state-icon>`
|
||||
: html`<ha-svg-icon
|
||||
.path=${helper.icon}
|
||||
style="color: var(--error-color)"
|
||||
|
||||
@@ -53,6 +53,7 @@ import "./ha-domain-integrations";
|
||||
import "./ha-integration-list-item";
|
||||
import type { AddIntegrationDialogParams } from "./show-add-integration-dialog";
|
||||
import { showYamlIntegrationDialog } from "./show-add-integration-dialog";
|
||||
import { showSingleConfigEntryWarning } from "./show-single-config-entry-warning";
|
||||
|
||||
export interface IntegrationListItem {
|
||||
name: string;
|
||||
@@ -710,21 +711,8 @@ class AddIntegrationDialog extends LitElement {
|
||||
});
|
||||
if (configEntries.length > 0) {
|
||||
this.closeDialog();
|
||||
const localize = await this.hass.loadBackendTranslation(
|
||||
"title",
|
||||
integration.name
|
||||
);
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.single_config_entry_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.single_config_entry",
|
||||
{
|
||||
integration_name: domainToName(localize, integration.name),
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
showSingleConfigEntryWarning(this, { domain: integration.domain });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-button";
|
||||
import { internationalizationContext } from "../../../data/context";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import { DialogMixin } from "../../../dialogs/dialog-mixin";
|
||||
import type { SingleConfigEntryWarningDialogParams } from "./show-single-config-entry-warning";
|
||||
|
||||
@customElement("dialog-single-config-entry-warning")
|
||||
class DialogSingleConfigEntryWarning extends DialogMixin<SingleConfigEntryWarningDialogParams>(
|
||||
LitElement
|
||||
) {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state() private _backendLocalize?: LocalizeFunc;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._loadBackendLocalize();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params || !this._backendLocalize) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
.headerTitle=${this._i18n.localize(
|
||||
"ui.panel.config.integrations.config_flow.single_config_entry_title"
|
||||
)}
|
||||
>
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.integrations.config_flow.single_config_entry",
|
||||
{
|
||||
integration_name: html`<b
|
||||
>${domainToName(this._backendLocalize, this.params.domain)}</b
|
||||
>`,
|
||||
}
|
||||
)}
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this._i18n.localize("ui.common.close")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
.href=${`/config/integrations/integration/${this.params.domain}`}
|
||||
>
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.integrations.config_flow.show_integration"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _loadBackendLocalize() {
|
||||
if (!this.params) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._backendLocalize = await this._i18n.loadBackendTranslation(
|
||||
"title",
|
||||
this.params.domain
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-single-config-entry-warning": DialogSingleConfigEntryWarning;
|
||||
}
|
||||
}
|
||||
@@ -621,7 +621,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
${this._manifest?.integration_type !== "hardware"
|
||||
${this._manifest?.integration_type !== "hardware" &&
|
||||
(!this._manifest?.single_config_entry ||
|
||||
(normalData.length === 0 && attentionData.length === 0))
|
||||
? html`<ha-button
|
||||
.appearance=${canAddDevice ? "filled" : "accent"}
|
||||
@click=${this._addIntegration}
|
||||
@@ -1235,30 +1237,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this._manifest?.single_config_entry) {
|
||||
const entries = this._domainConfigEntries(
|
||||
this.domain,
|
||||
this._extraConfigEntries || this.configEntries
|
||||
);
|
||||
if (entries.length > 0) {
|
||||
const localize = await this.hass.loadBackendTranslation(
|
||||
"title",
|
||||
this._manifest.name
|
||||
);
|
||||
await showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.single_config_entry_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.single_config_entry",
|
||||
{
|
||||
integration_name: domainToName(localize, this._manifest.name),
|
||||
}
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
showAddIntegrationDialog(this, {
|
||||
domain: this.domain,
|
||||
navigateToResult: true,
|
||||
|
||||
@@ -69,6 +69,7 @@ import "./ha-integration-card";
|
||||
import type { HaIntegrationCard } from "./ha-integration-card";
|
||||
import "./ha-integration-overflow-menu";
|
||||
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
|
||||
import { showSingleConfigEntryWarning } from "./show-single-config-entry-warning";
|
||||
|
||||
export interface ConfigEntryExtended extends Omit<ConfigEntry, "entry_id"> {
|
||||
entry_id?: string;
|
||||
@@ -914,21 +915,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
if (integration.single_config_entry) {
|
||||
const configEntries = await getConfigEntries(this.hass, { domain });
|
||||
if (configEntries.length > 0) {
|
||||
const localize = await this.hass.loadBackendTranslation(
|
||||
"title",
|
||||
integration.name
|
||||
);
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.single_config_entry_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.single_config_entry",
|
||||
{
|
||||
integration_name: domainToName(localize, integration.name!),
|
||||
}
|
||||
),
|
||||
});
|
||||
showSingleConfigEntryWarning(this, { domain });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +165,12 @@ export class HaIntegrationCard extends LitElement {
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip
|
||||
for="icon-custom"
|
||||
.placement=${computeRTL(this.hass) ? "right" : "left"}
|
||||
.placement=${computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? "right"
|
||||
: "left"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
this.manifest.overwrites_built_in
|
||||
@@ -180,7 +185,12 @@ export class HaIntegrationCard extends LitElement {
|
||||
<ha-svg-icon id="icon-cloud" .path=${mdiWeb}></ha-svg-icon>
|
||||
<ha-tooltip
|
||||
for="icon-cloud"
|
||||
.placement=${computeRTL(this.hass) ? "right" : "left"}
|
||||
.placement=${computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? "right"
|
||||
: "left"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.depends_on_cloud"
|
||||
@@ -198,7 +208,12 @@ export class HaIntegrationCard extends LitElement {
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip
|
||||
for="icon-yaml"
|
||||
.placement=${computeRTL(this.hass) ? "right" : "left"}
|
||||
.placement=${computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? "right"
|
||||
: "left"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.no_config_flow"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { LitElement } from "lit";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
export interface SingleConfigEntryWarningDialogParams {
|
||||
domain: string;
|
||||
}
|
||||
|
||||
export const showSingleConfigEntryWarning = (
|
||||
element: LitElement,
|
||||
params: SingleConfigEntryWarningDialogParams
|
||||
) =>
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-single-config-entry-warning",
|
||||
dialogParams: params,
|
||||
dialogImport: () => import("./dialog-single-config-entry-warning"),
|
||||
});
|
||||
@@ -280,10 +280,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
showNarrow: true,
|
||||
type: "icon",
|
||||
template: (scene) => html`
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${scene}
|
||||
></ha-state-icon>
|
||||
<ha-state-icon .stateObj=${scene}></ha-state-icon>
|
||||
`,
|
||||
},
|
||||
name: {
|
||||
|
||||
@@ -343,7 +343,10 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
return html` <div
|
||||
id="root"
|
||||
class=${classMap({
|
||||
rtl: computeRTL(this.hass),
|
||||
rtl: computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
),
|
||||
})}
|
||||
>
|
||||
${this._config
|
||||
|
||||
@@ -287,7 +287,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
type: "icon",
|
||||
template: (script) =>
|
||||
html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${script}
|
||||
style=${styleMap({
|
||||
color:
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
@@ -21,6 +20,8 @@ import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-tab-group";
|
||||
import "../../../components/ha-tab-group-tab";
|
||||
import "../../../components/trace/ha-trace-blueprint-config";
|
||||
import "../../../components/trace/ha-trace-config";
|
||||
import "../../../components/trace/ha-trace-logbook";
|
||||
@@ -31,7 +32,6 @@ import type {
|
||||
HatScriptGraph,
|
||||
NodeInfo,
|
||||
} from "../../../components/trace/hat-script-graph";
|
||||
import { traceTabStyles } from "../../../components/trace/trace-tab-styles";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import type { LogbookEntry } from "../../../data/logbook";
|
||||
@@ -46,6 +46,8 @@ import type { HomeAssistant, Route } from "../../../types";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import "../../../components/ha-trace-picker";
|
||||
|
||||
const TABS = ["details", "timeline", "logbook", "config"] as const;
|
||||
|
||||
@customElement("ha-script-trace")
|
||||
export class HaScriptTrace extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -76,12 +78,7 @@ export class HaScriptTrace extends LitElement {
|
||||
|
||||
@state() private _logbookEntries?: LogbookEntry[];
|
||||
|
||||
@state() private _view:
|
||||
| "details"
|
||||
| "config"
|
||||
| "timeline"
|
||||
| "logbook"
|
||||
| "blueprint" = "details";
|
||||
@state() private _view: (typeof TABS)[number] | "blueprint" = "details";
|
||||
|
||||
@query("hat-script-graph") private _graph?: HatScriptGraph;
|
||||
|
||||
@@ -221,61 +218,36 @@ export class HaScriptTrace extends LitElement {
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<div class="tabs top">
|
||||
${[
|
||||
[
|
||||
"details",
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.trace.tabs.details"
|
||||
),
|
||||
],
|
||||
[
|
||||
"timeline",
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.trace.tabs.timeline"
|
||||
),
|
||||
],
|
||||
[
|
||||
"logbook",
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.trace.tabs.logbook"
|
||||
),
|
||||
],
|
||||
[
|
||||
"config",
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.trace.tabs.script_config"
|
||||
),
|
||||
],
|
||||
].map(
|
||||
([view, label]) => html`
|
||||
<button
|
||||
tabindex="0"
|
||||
.view=${view}
|
||||
class=${classMap({
|
||||
active: this._view === view,
|
||||
})}
|
||||
@click=${this._showTab}
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${TABS.map(
|
||||
(view) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === view}
|
||||
.panel=${view}
|
||||
>
|
||||
${label}
|
||||
</button>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.trace.tabs.${
|
||||
view === "config" ? "script_config" : view
|
||||
}`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
${this._trace.blueprint_inputs
|
||||
? html`
|
||||
<button
|
||||
tabindex="0"
|
||||
.view=${"blueprint"}
|
||||
class=${classMap({
|
||||
active: this._view === "blueprint",
|
||||
})}
|
||||
@click=${this._showTab}
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === "blueprint"}
|
||||
panel="blueprint"
|
||||
>
|
||||
Blueprint Config
|
||||
</button>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.automation.trace.tabs.blueprint_config`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</ha-tab-group>
|
||||
${this._selected === undefined ||
|
||||
this._logbookEntries === undefined ||
|
||||
trackedNodes === undefined
|
||||
@@ -499,8 +471,8 @@ export class HaScriptTrace extends LitElement {
|
||||
this._logbookEntries = traceInfo.logbookEntries;
|
||||
}
|
||||
|
||||
private _showTab(ev: Event) {
|
||||
this._view = (ev.target as any).view;
|
||||
private _handleTabChanged(ev: CustomEvent) {
|
||||
this._view = ev.detail.name as typeof this._view;
|
||||
}
|
||||
|
||||
private _timelinePathPicked(ev: CustomEvent) {
|
||||
@@ -550,7 +522,6 @@ export class HaScriptTrace extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
traceTabStyles,
|
||||
css`
|
||||
.toolbar {
|
||||
display: flex;
|
||||
@@ -609,6 +580,14 @@ export class HaScriptTrace extends LitElement {
|
||||
overflow-y: auto;
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
ha-tab-group {
|
||||
background-color: var(--primary-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-tab-group-tab::part(base) {
|
||||
padding: 2px 16px;
|
||||
}
|
||||
:host([narrow]) .info {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import "@lit-labs/virtualizer";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeEntityNameList } from "../../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
@@ -17,11 +17,17 @@ import "../../../components/ha-list";
|
||||
import "../../../components/ha-state-icon";
|
||||
import "../../../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../../../components/input/ha-input-search";
|
||||
import {
|
||||
configContext,
|
||||
internationalizationContext,
|
||||
registriesContext,
|
||||
statesContext,
|
||||
} from "../../../data/context";
|
||||
import type { ExposeEntitySettings } from "../../../data/expose";
|
||||
import { voiceAssistants } from "../../../data/expose";
|
||||
import { DialogMixin } from "../../../dialogs/dialog-mixin";
|
||||
import { haStyle, haStyleScrollbar } from "../../../resources/styles";
|
||||
import { loadVirtualizer } from "../../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./entity-voice-settings";
|
||||
import type { ExposeEntityDialogParams } from "./show-dialog-expose-entity";
|
||||
|
||||
@@ -31,68 +37,58 @@ interface FilteredEntity {
|
||||
}
|
||||
|
||||
@customElement("dialog-expose-entity")
|
||||
class DialogExposeEntity extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: ExposeEntityDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
class DialogExposeEntity extends DialogMixin<ExposeEntityDialogParams>(
|
||||
LitElement
|
||||
) {
|
||||
@state() private _filter?: string;
|
||||
|
||||
@state() private _selected: string[] = [];
|
||||
|
||||
public willUpdate(): void {
|
||||
if (!this.hasUpdated) {
|
||||
loadVirtualizer();
|
||||
}
|
||||
}
|
||||
@state() private _dialogReady = false;
|
||||
|
||||
public async showDialog(params: ExposeEntityDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
}
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
protected _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
protected _config!: ContextType<typeof configContext>;
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
this._selected = [];
|
||||
this._filter = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
protected _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@consume({ context: registriesContext, subscribe: true })
|
||||
protected _registries!: ContextType<typeof registriesContext>;
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
if (!this.params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const header = this.hass.localize(
|
||||
const header = this._i18n.localize(
|
||||
"ui.panel.config.voice_assistants.expose.expose_dialog.header"
|
||||
);
|
||||
const subtitle = this.hass.localize(
|
||||
const subtitle = this._i18n.localize(
|
||||
"ui.panel.config.voice_assistants.expose.expose_dialog.expose_to",
|
||||
{
|
||||
assistants: this._params.filterAssistants
|
||||
assistants: this.params.filterAssistants
|
||||
.map((ass) => voiceAssistants[ass].name)
|
||||
.join(", "),
|
||||
}
|
||||
);
|
||||
|
||||
const entities = this._filterEntities(
|
||||
this._params.exposedEntities,
|
||||
this.params.exposedEntities,
|
||||
this._filter
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
open
|
||||
header-title=${header}
|
||||
header-subtitle=${subtitle}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
@after-show=${this._loadVirtualizer}
|
||||
>
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
@@ -100,15 +96,18 @@ class DialogExposeEntity extends LitElement {
|
||||
@input=${this._filterChanged}
|
||||
></ha-input-search>
|
||||
<ha-list multi>
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="ha-scrollbar"
|
||||
@click=${this._itemClicked}
|
||||
@keydown=${this._handleItemKeydown}
|
||||
.items=${entities}
|
||||
.renderItem=${this._renderItem}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
${this._dialogReady
|
||||
? html` <lit-virtualizer
|
||||
scroller
|
||||
class="ha-scrollbar"
|
||||
@click=${this._itemClicked}
|
||||
@keydown=${this._handleItemKeydown}
|
||||
.items=${entities}
|
||||
.renderItem=${this._renderItem}
|
||||
.keyFunction=${this._keyFunction}
|
||||
>
|
||||
</lit-virtualizer>`
|
||||
: nothing}
|
||||
</ha-list>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
@@ -116,14 +115,14 @@ class DialogExposeEntity extends LitElement {
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
${this._i18n.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._expose}
|
||||
.disabled=${this._selected.length === 0}
|
||||
>
|
||||
${this.hass.localize(
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.voice_assistants.expose.expose_dialog.expose_entities",
|
||||
{ count: this._selected.length }
|
||||
)}
|
||||
@@ -133,6 +132,13 @@ class DialogExposeEntity extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _loadVirtualizer() {
|
||||
await loadVirtualizer();
|
||||
this._dialogReady = true;
|
||||
}
|
||||
|
||||
private _keyFunction = (item: FilteredEntity) => item.entity.entity_id;
|
||||
|
||||
private _handleSelected = (ev) => {
|
||||
const entityId = ev.target.value;
|
||||
if (ev.detail.selected) {
|
||||
@@ -169,9 +175,9 @@ class DialogExposeEntity extends LitElement {
|
||||
const lowerFilter = filter?.toLowerCase();
|
||||
const result: FilteredEntity[] = [];
|
||||
|
||||
for (const entity of Object.values(this.hass.states)) {
|
||||
for (const entity of Object.values(this._states)) {
|
||||
if (
|
||||
this._params!.filterAssistants.every(
|
||||
this.params!.filterAssistants.every(
|
||||
(ass) => exposedEntities[entity.entity_id]?.[ass]
|
||||
)
|
||||
) {
|
||||
@@ -181,10 +187,10 @@ class DialogExposeEntity extends LitElement {
|
||||
const nameList = computeEntityNameList(
|
||||
entity,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
this._registries.entities,
|
||||
this._registries.devices,
|
||||
this._registries.areas,
|
||||
this._registries.floors
|
||||
);
|
||||
|
||||
if (!lowerFilter) {
|
||||
@@ -224,12 +230,15 @@ class DialogExposeEntity extends LitElement {
|
||||
const { entity: entityState, nameList } = item;
|
||||
const [entityName, deviceName, areaName] = nameList;
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isRTL = computeRTL(
|
||||
this._i18n.language,
|
||||
this._i18n.translationMetadata.translations
|
||||
);
|
||||
const primary = entityName || deviceName || entityState.entity_id;
|
||||
const context = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
const showEntityId = this.hass.userData?.showEntityIdPicker;
|
||||
const showEntityId = this._config?.userData?.showEntityIdPicker;
|
||||
|
||||
return html`
|
||||
<ha-check-list-item
|
||||
@@ -244,7 +253,6 @@ class DialogExposeEntity extends LitElement {
|
||||
<ha-state-icon
|
||||
title=${ifDefined(entityState?.state)}
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entityState}
|
||||
></ha-state-icon>
|
||||
${primary}
|
||||
@@ -263,7 +271,7 @@ class DialogExposeEntity extends LitElement {
|
||||
};
|
||||
|
||||
private _expose() {
|
||||
this._params!.exposeEntities(this._selected);
|
||||
this.params!.exposeEntities(this._selected);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,6 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
<ha-state-icon
|
||||
title=${ifDefined(entry.entity?.state)}
|
||||
.stateObj=${entry.entity}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
`,
|
||||
},
|
||||
|
||||
@@ -217,7 +217,6 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
: html`
|
||||
<ha-state-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.icon=${this._config.icon}
|
||||
></ha-state-icon>
|
||||
|
||||
@@ -69,12 +69,7 @@ export class HuiShortcutBadge extends LitElement implements LovelaceBadge {
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
handleAction(
|
||||
ev.currentTarget as HTMLElement,
|
||||
this.hass!,
|
||||
this._config!,
|
||||
ev.detail.action!
|
||||
);
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
private get _hasAction() {
|
||||
|
||||
@@ -17,12 +17,12 @@ import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-domain-icon";
|
||||
import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import {
|
||||
AREA_CONTROLS_BUTTONS,
|
||||
getAreaControlEntities,
|
||||
MAX_DEFAULT_AREA_CONTROLS,
|
||||
} from "../../../data/area/area_controls";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
import { computeCssVariable } from "../../../resources/css-variables";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@@ -293,10 +293,7 @@ class HuiAreaControlsCardFeature
|
||||
.deviceClass=${deviceClass}
|
||||
.state=${entityState}
|
||||
></ha-domain-icon>`
|
||||
: html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entity}
|
||||
></ha-state-icon>`}
|
||||
: html`<ha-state-icon .stateObj=${entity}></ha-state-icon>`}
|
||||
</ha-control-button>
|
||||
`;
|
||||
})}
|
||||
|
||||
@@ -8,7 +8,7 @@ import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-control-number-buttons";
|
||||
import "../../../components/ha-control-slider";
|
||||
import "../../../components/ha-icon";
|
||||
import { isUnavailableState } from "../../../data/entity/entity";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
@@ -117,7 +117,7 @@ class HuiNumericInputCardFeature
|
||||
.max=${stateObj.attributes.max}
|
||||
.step=${stateObj.attributes.step}
|
||||
@value-changed=${this._setValue}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
.unit=${stateObj.attributes.unit_of_measurement}
|
||||
.locale=${this.hass.locale}
|
||||
></ha-control-number-buttons>
|
||||
@@ -130,7 +130,7 @@ class HuiNumericInputCardFeature
|
||||
.max=${stateObj.attributes.max}
|
||||
.step=${stateObj.attributes.step}
|
||||
@value-changed=${this._setValue}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
.unit=${stateObj.attributes.unit_of_measurement}
|
||||
.locale=${this.hass.locale}
|
||||
></ha-control-slider>
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing, svg } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { batteryLevelIconPath } from "../../../../common/entity/battery_icon";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
@@ -37,6 +38,9 @@ import type { EnergyDistributionCardConfig } from "../types";
|
||||
|
||||
const CIRCLE_CIRCUMFERENCE = 238.76104;
|
||||
|
||||
const periodIncludesNow = (data: EnergyData): boolean =>
|
||||
!data.end || data.end.getTime() >= Date.now();
|
||||
|
||||
@customElement("hui-energy-distribution-card")
|
||||
class HuiEnergyDistrubutionCard
|
||||
extends SubscribeMixin(LitElement)
|
||||
@@ -100,14 +104,34 @@ class HuiEnergyDistrubutionCard
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return (
|
||||
if (
|
||||
hasConfigChanged(this, changedProps) ||
|
||||
changedProps.size > 1 ||
|
||||
!changedProps.has("hass") ||
|
||||
(!!this._data?.co2SignalEntity &&
|
||||
this.hass.states[this._data.co2SignalEntity] !==
|
||||
changedProps.get("hass").states[this._data.co2SignalEntity])
|
||||
);
|
||||
!changedProps.has("hass")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const oldStates = changedProps.get("hass").states;
|
||||
if (
|
||||
this._data?.co2SignalEntity &&
|
||||
this.hass.states[this._data.co2SignalEntity] !==
|
||||
oldStates[this._data.co2SignalEntity]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (this._data && periodIncludesNow(this._data)) {
|
||||
const batteries = energySourcesByType(this._data.prefs).battery;
|
||||
if (
|
||||
batteries?.some(
|
||||
(source) =>
|
||||
source.stat_soc &&
|
||||
this.hass.states[source.stat_soc] !== oldStates[source.stat_soc]
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected willUpdate() {
|
||||
@@ -174,10 +198,29 @@ class HuiEnergyDistrubutionCard
|
||||
|
||||
let totalBatteryIn: number | null = null;
|
||||
let totalBatteryOut: number | null = null;
|
||||
let batteryIconPath = mdiBatteryHigh;
|
||||
|
||||
if (hasBattery) {
|
||||
totalBatteryIn = summedData.total.to_battery ?? 0;
|
||||
totalBatteryOut = summedData.total.from_battery ?? 0;
|
||||
|
||||
// The SOC reflects the current battery level, so it only matches the
|
||||
// card's data when the selected period extends to now. For historical
|
||||
// periods (yesterday, last week, ...) fall back to the generic icon.
|
||||
if (periodIncludesNow(this._data)) {
|
||||
const socValues = types
|
||||
.battery!.map((source) =>
|
||||
source.stat_soc
|
||||
? Number(this.hass.states[source.stat_soc]?.state)
|
||||
: NaN
|
||||
)
|
||||
.filter((value) => Number.isFinite(value));
|
||||
if (socValues.length) {
|
||||
const averageSoc =
|
||||
socValues.reduce((sum, value) => sum + value, 0) / socValues.length;
|
||||
batteryIconPath = batteryLevelIconPath(averageSoc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let returnedToGrid: number | null = null;
|
||||
@@ -569,7 +612,7 @@ class HuiEnergyDistrubutionCard
|
||||
${hasBattery
|
||||
? html` <div class="circle-container battery">
|
||||
<div class="circle">
|
||||
<ha-svg-icon .path=${mdiBatteryHigh}></ha-svg-icon>
|
||||
<ha-svg-icon .path=${batteryIconPath}></ha-svg-icon>
|
||||
<span class="battery-in">
|
||||
<ha-svg-icon
|
||||
class="small"
|
||||
|
||||
@@ -247,11 +247,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
|
||||
@click=${this._handleMoreInfo}
|
||||
.label=${stateLabel}
|
||||
>
|
||||
<ha-state-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-icon>
|
||||
<ha-state-icon slot="icon" .stateObj=${stateObj}></ha-state-icon>
|
||||
</ha-assist-chip>
|
||||
</h1>
|
||||
<div id="armActions" class="actions">
|
||||
|
||||
@@ -16,10 +16,6 @@ import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { BINARY_STATE_ON, STRINGS_SEPARATOR_DOT } from "../../../common/const";
|
||||
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||
import { generateEntityFilter } from "../../../common/entity/entity_filter";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import {
|
||||
formatNumber,
|
||||
isNumericState,
|
||||
@@ -37,9 +33,13 @@ import "../../../components/tile/ha-tile-container";
|
||||
import "../../../components/tile/ha-tile-icon";
|
||||
import "../../../components/tile/ha-tile-info";
|
||||
import { isUnavailableState } from "../../../data/entity/entity";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../card-features/hui-card-features";
|
||||
import type { LovelaceCardFeatureContext } from "../card-features/types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import type {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
@@ -373,7 +373,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
|
||||
return html`
|
||||
<ha-tile-badge class="alert-badge">
|
||||
<ha-state-icon .hass=${this.hass} .stateObj=${stateObj}></ha-state-icon>
|
||||
<ha-state-icon .stateObj=${stateObj}></ha-state-icon>
|
||||
</ha-tile-badge>
|
||||
`;
|
||||
}
|
||||
@@ -389,10 +389,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
${states.map(
|
||||
(stateObj) => html`
|
||||
<div class="alert">
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-icon>
|
||||
<ha-state-icon .stateObj=${stateObj}></ha-state-icon>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
|
||||
@@ -201,7 +201,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
)}
|
||||
data-state=${ifDefined(stateObj?.state)}
|
||||
.icon=${this._config.icon}
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
style=${styleMap({
|
||||
filter: stateObj ? stateColorBrightness(stateObj) : undefined,
|
||||
|
||||
@@ -20,12 +20,12 @@ import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../../data/climate";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction, hasAnyAction } from "../common/has-action";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction, hasAnyAction } from "../common/has-action";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { createHeaderFooterElement } from "../create-element/create-header-footer-element";
|
||||
@@ -159,7 +159,6 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
||||
<ha-state-icon
|
||||
.icon=${this._config.icon}
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
data-domain=${ifDefined(domain)}
|
||||
data-state=${stateObj.state}
|
||||
style=${styleMap({
|
||||
|
||||
@@ -143,7 +143,6 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
<ha-state-icon
|
||||
.icon=${this._config.icon}
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
|
||||
@@ -235,11 +235,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
>
|
||||
<div class="top-info">
|
||||
<div class="icon-name">
|
||||
<ha-state-icon
|
||||
class="icon"
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
<ha-state-icon class="icon" .stateObj=${stateObj}></ha-state-icon>
|
||||
<div>
|
||||
${this.hass.formatEntityName(
|
||||
this.hass!.states[this._config!.entity],
|
||||
|
||||
@@ -307,7 +307,6 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
|
||||
<ha-state-icon
|
||||
.icon=${entityConf.icon}
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
</ha-icon-button>
|
||||
|
||||
|
||||
@@ -95,12 +95,7 @@ export class HuiShortcutCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
handleAction(
|
||||
ev.currentTarget as HTMLElement,
|
||||
this.hass!,
|
||||
this._config!,
|
||||
ev.detail.action!
|
||||
);
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
private get _hasCardAction() {
|
||||
|
||||
@@ -211,7 +211,6 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard {
|
||||
<ha-state-icon
|
||||
.icon=${this._config.icon}
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -313,7 +313,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
slot="icon"
|
||||
.icon=${this._config.icon}
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
`}
|
||||
${renderTileBadge(stateObj, this.hass)}
|
||||
|
||||
@@ -5,9 +5,9 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { DragScrollController } from "../../../common/controllers/drag-scroll-controller";
|
||||
import { formatDateWeekdayShort } from "../../../common/datetime/format_date";
|
||||
import { formatTime } from "../../../common/datetime/format_time";
|
||||
import { DragScrollController } from "../../../common/controllers/drag-scroll-controller";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
@@ -327,7 +327,6 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
<ha-state-icon
|
||||
class="weather-icon"
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
`}
|
||||
</div>
|
||||
|
||||
@@ -9,9 +9,6 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { toggleEntity } from "./entity/toggle-entity";
|
||||
|
||||
const loadLovelacePopupDialog = () =>
|
||||
import("../dialogs/dialog-lovelace-popup");
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"ll-custom": ActionConfig;
|
||||
@@ -186,27 +183,6 @@ export const handleAction = async (
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "show-popup": {
|
||||
if (!actionConfig.cards?.length) {
|
||||
showToast(node, {
|
||||
message: hass.localize(
|
||||
"ui.panel.lovelace.cards.actions.no_popup_cards"
|
||||
),
|
||||
});
|
||||
forwardHaptic(node, "failure");
|
||||
return;
|
||||
}
|
||||
fireEvent(node, "show-dialog", {
|
||||
dialogTag: "dialog-lovelace-popup",
|
||||
dialogImport: loadLovelacePopupDialog,
|
||||
dialogAnchor: node,
|
||||
dialogParams: {
|
||||
hass,
|
||||
cards: actionConfig.cards,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "fire-dom-event": {
|
||||
fireEvent(node, "ll-custom", actionConfig);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { refine } from "superstruct";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-assist-pipeline-picker";
|
||||
import type {
|
||||
@@ -21,18 +20,14 @@ import type {
|
||||
NavigateActionConfig,
|
||||
UrlActionConfig,
|
||||
} from "../../../data/lovelace/config/action";
|
||||
import type { LovelaceConfig } from "../../../data/lovelace/config/types";
|
||||
import type { ServiceAction } from "../../../data/script";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../editor/card-editor/hui-card-list-editor";
|
||||
import type { CardsChangedEvent } from "../editor/card-editor/hui-card-list-editor";
|
||||
|
||||
export type UiAction = Exclude<ActionConfig["action"], "fire-dom-event">;
|
||||
|
||||
export interface ActionRelatedContext {
|
||||
entity_id?: string;
|
||||
area_id?: string;
|
||||
lovelace?: LovelaceConfig;
|
||||
}
|
||||
|
||||
export const ACTION_RELATED_CONTEXT = {
|
||||
@@ -240,16 +235,6 @@ export class HuiActionEditor extends LitElement {
|
||||
</ha-form>
|
||||
`
|
||||
: nothing}
|
||||
${this.config?.action === "show-popup"
|
||||
? html`
|
||||
<hui-card-list-editor
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this.context?.lovelace}
|
||||
.cards=${this.config.cards ?? []}
|
||||
@cards-changed=${this._popupCardsChanged}
|
||||
></hui-card-list-editor>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -288,12 +273,6 @@ export class HuiActionEditor extends LitElement {
|
||||
data = { navigation_path: this._navigation_path };
|
||||
break;
|
||||
}
|
||||
case "show-popup": {
|
||||
data = {
|
||||
cards: this.config?.action === "show-popup" ? this.config.cards : [],
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
@@ -328,19 +307,7 @@ export class HuiActionEditor extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _popupCardsChanged(ev: HASSDomEvent<CardsChangedEvent>) {
|
||||
ev.stopPropagation();
|
||||
if (this.config?.action !== "show-popup") {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.config, cards: ev.detail.cards },
|
||||
});
|
||||
}
|
||||
|
||||
private _computeFormLabel(
|
||||
schema: SchemaUnion<typeof ASSIST_SCHEMA> | HaFormSchema
|
||||
) {
|
||||
private _computeFormLabel(schema: SchemaUnion<typeof ASSIST_SCHEMA>) {
|
||||
return this.hass?.localize(
|
||||
`ui.panel.lovelace.editor.action-editor.${schema.name}`
|
||||
);
|
||||
@@ -399,10 +366,6 @@ export class HuiActionEditor extends LitElement {
|
||||
ha-service-control {
|
||||
--service-control-padding: 0;
|
||||
}
|
||||
hui-card-list-editor {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,10 @@ export class HuiEntityEditor extends LitElement {
|
||||
stateObj &&
|
||||
entityUseDeviceName(stateObj, this.hass.entities, this.hass.devices);
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const primary =
|
||||
(stateObj &&
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
import "@home-assistant/webawesome/dist/components/popover/popover";
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../../../common/dom/media_query";
|
||||
import { ADAPTIVE_DIALOG_MEDIA_QUERY } from "../../../components/ha-adaptive-dialog";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-bottom-sheet";
|
||||
import { DialogMixin } from "../../../dialogs/dialog-mixin";
|
||||
import "../sections/hui-section";
|
||||
|
||||
type WaPopoverElement = HTMLElement & {
|
||||
anchor: Element | null;
|
||||
};
|
||||
|
||||
export interface LovelacePopupDialogParams {
|
||||
hass: HomeAssistant;
|
||||
cards: LovelaceCardConfig[];
|
||||
}
|
||||
|
||||
@customElement("dialog-lovelace-popup")
|
||||
export class DialogLovelacePopup extends DialogMixin<LovelacePopupDialogParams>(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public params?: LovelacePopupDialogParams;
|
||||
|
||||
@property({ attribute: false }) public override dialogAnchor?: Element;
|
||||
|
||||
@state() private _narrow = false;
|
||||
|
||||
@state() private _open = true;
|
||||
|
||||
@state() private _popoverOpen = false;
|
||||
|
||||
private _unsubMediaQuery?: () => void;
|
||||
|
||||
private _openPopoverAnimationFrame?: number;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._unsubMediaQuery = listenMediaQuery(
|
||||
ADAPTIVE_DIALOG_MEDIA_QUERY,
|
||||
(matches) => {
|
||||
this._narrow = matches;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this._cancelScheduledPopoverOpen();
|
||||
this._unsubMediaQuery?.();
|
||||
this._unsubMediaQuery = undefined;
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("params")) {
|
||||
this._open = true;
|
||||
this._popoverOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected updated() {
|
||||
if (this._presentationMode !== "popover") {
|
||||
this._cancelScheduledPopoverOpen();
|
||||
this._popoverOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._syncPopoverAnchor();
|
||||
|
||||
if (!this._open) {
|
||||
this._cancelScheduledPopoverOpen();
|
||||
this._popoverOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._popoverOpen || this._openPopoverAnimationFrame !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._openPopoverAnimationFrame = requestAnimationFrame(() => {
|
||||
this._openPopoverAnimationFrame = undefined;
|
||||
this._syncPopoverAnchor();
|
||||
|
||||
if (this._open && this._presentationMode === "popover") {
|
||||
this._popoverOpen = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _cancelScheduledPopoverOpen() {
|
||||
if (this._openPopoverAnimationFrame === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelAnimationFrame(this._openPopoverAnimationFrame);
|
||||
this._openPopoverAnimationFrame = undefined;
|
||||
}
|
||||
|
||||
private _syncPopoverAnchor() {
|
||||
const popover =
|
||||
this.renderRoot.querySelector<WaPopoverElement>("wa-popover");
|
||||
const anchor = this.dialogAnchor ?? null;
|
||||
if (popover && popover.anchor !== anchor) {
|
||||
popover.anchor = anchor;
|
||||
}
|
||||
}
|
||||
|
||||
public override closeDialog(_historyState?: any): Promise<boolean> | boolean {
|
||||
if (this._presentationMode === "popover") {
|
||||
this._open = false;
|
||||
this._popoverOpen = false;
|
||||
return true;
|
||||
}
|
||||
return super.closeDialog(_historyState);
|
||||
}
|
||||
|
||||
private get _presentationMode(): "popover" | "bottom-sheet" {
|
||||
return this._narrow ? "bottom-sheet" : "popover";
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const presentationMode = this._presentationMode;
|
||||
const popupLabel = this.params.hass.localize(
|
||||
"ui.panel.lovelace.editor.action-editor.actions.show-popup"
|
||||
);
|
||||
|
||||
const content = html`<hui-section
|
||||
.hass=${this.params.hass}
|
||||
.config=${{ cards: this.params.cards }}
|
||||
.index=${0}
|
||||
.viewIndex=${0}
|
||||
import-only
|
||||
></hui-section>`;
|
||||
|
||||
if (presentationMode === "bottom-sheet") {
|
||||
return html`<ha-bottom-sheet .open=${this._open} aria-label=${popupLabel}
|
||||
>${content}</ha-bottom-sheet
|
||||
>`;
|
||||
}
|
||||
|
||||
return html`<wa-popover
|
||||
.open=${this._popoverOpen}
|
||||
.anchor=${this.dialogAnchor ?? null}
|
||||
auto-size="vertical"
|
||||
auto-size-padding="16"
|
||||
placement="bottom"
|
||||
without-arrow
|
||||
trap-focus
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label=${popupLabel}
|
||||
@wa-show=${this._handlePopoverShow}
|
||||
@wa-after-hide=${this._handlePopoverAfterHide}
|
||||
>
|
||||
<div class="popover-surface">${content}</div>
|
||||
</wa-popover>`;
|
||||
}
|
||||
|
||||
private _handlePopoverShow(ev: Event) {
|
||||
if (ev.eventPhase === Event.AT_TARGET) {
|
||||
this._open = true;
|
||||
fireEvent(this, "opened");
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePopoverAfterHide(ev: Event) {
|
||||
if (ev.eventPhase !== Event.AT_TARGET) {
|
||||
return;
|
||||
}
|
||||
this._open = false;
|
||||
this._popoverOpen = false;
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-bottom-sheet {
|
||||
--dialog-content-padding: var(--ha-space-4);
|
||||
--ha-bottom-sheet-content-padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
wa-popover {
|
||||
--width: min(var(--ha-dialog-width-lg, 1024px), 95vw);
|
||||
--wa-color-surface-raised: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--card-background-color, var(--ha-color-surface-default))
|
||||
);
|
||||
--wa-panel-border-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-3xl)
|
||||
);
|
||||
}
|
||||
|
||||
wa-popover::part(dialog)::backdrop {
|
||||
background: none;
|
||||
}
|
||||
|
||||
wa-popover::part(body) {
|
||||
padding: 0;
|
||||
border-color: transparent;
|
||||
box-shadow: var(--dialog-box-shadow, var(--wa-shadow-l));
|
||||
min-width: var(--width);
|
||||
max-width: var(--width);
|
||||
max-height: calc(var(--safe-height) - var(--ha-space-20));
|
||||
overflow: hidden;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.popover-surface {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: inherit;
|
||||
overflow: auto;
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
hui-section {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-lovelace-popup": DialogLovelacePopup;
|
||||
}
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
import {
|
||||
mdiCodeBraces,
|
||||
mdiContentCopy,
|
||||
mdiContentCut,
|
||||
mdiDelete,
|
||||
mdiListBoxOutline,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-icon-button-arrow-next";
|
||||
import "../../../../components/ha-icon-button-arrow-prev";
|
||||
import "../../../../components/ha-tab-group";
|
||||
import "../../../../components/ha-tab-group-tab";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { HuiCardElementEditor } from "./hui-card-element-editor";
|
||||
import "./hui-card-element-editor";
|
||||
import "./hui-card-picker";
|
||||
import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
import type { GUIModeChangedEvent } from "../types";
|
||||
|
||||
export interface CardsChangedEvent {
|
||||
cards: LovelaceCardConfig[];
|
||||
guiModeAvailable?: boolean;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"cards-changed": CardsChangedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hui-card-list-editor")
|
||||
export class HuiCardListEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
||||
|
||||
@property({ attribute: false }) public cards: LovelaceCardConfig[] = [];
|
||||
|
||||
@property({ type: Boolean, attribute: "show-copy-cut" })
|
||||
public showCopyCut = false;
|
||||
|
||||
@storage({
|
||||
key: "dashboardCardClipboard",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
protected _clipboard?: LovelaceCardConfig;
|
||||
|
||||
@state() private _selectedCard = 0;
|
||||
|
||||
@state() private _guiMode = true;
|
||||
|
||||
@state() private _guiModeAvailable? = true;
|
||||
|
||||
@query("hui-card-element-editor")
|
||||
private _cardEditorEl?: HuiCardElementEditor;
|
||||
|
||||
private _keys = new Map<string, string>();
|
||||
|
||||
public focusYamlEditor() {
|
||||
this._cardEditorEl?.focusYamlEditor();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const selected = this._selectedCard;
|
||||
const isGuiMode = !this._cardEditorEl || this._guiMode;
|
||||
|
||||
return html`
|
||||
<div class="card-config">
|
||||
<div class="toolbar">
|
||||
<ha-tab-group @wa-tab-show=${this._handleSelectedCard}>
|
||||
${this.cards.map(
|
||||
(_card, index) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${index}
|
||||
.active=${index === selected}
|
||||
>
|
||||
${index + 1}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
</ha-tab-group>
|
||||
<ha-icon-button
|
||||
@click=${this._handleAddCard}
|
||||
.path=${mdiPlus}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.add_card"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
|
||||
<div class="editor">
|
||||
${selected < this.cards.length
|
||||
? html`
|
||||
<div class="card-options">
|
||||
<ha-icon-button
|
||||
class="gui-mode-button"
|
||||
@click=${this._toggleMode}
|
||||
.disabled=${!this._guiModeAvailable}
|
||||
.label=${this.hass!.localize(
|
||||
isGuiMode
|
||||
? "ui.panel.lovelace.editor.edit_card.show_code_editor"
|
||||
: "ui.panel.lovelace.editor.edit_card.show_visual_editor"
|
||||
)}
|
||||
.path=${isGuiMode ? mdiCodeBraces : mdiListBoxOutline}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-icon-button-arrow-prev
|
||||
.disabled=${selected === 0}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.move_before"
|
||||
)}
|
||||
@click=${this._handleMove}
|
||||
.move=${-1}
|
||||
></ha-icon-button-arrow-prev>
|
||||
|
||||
<ha-icon-button-arrow-next
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.move_after"
|
||||
)}
|
||||
.disabled=${selected === this.cards.length - 1}
|
||||
@click=${this._handleMove}
|
||||
.move=${1}
|
||||
></ha-icon-button-arrow-next>
|
||||
|
||||
${this.showCopyCut
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.copy"
|
||||
)}
|
||||
.path=${mdiContentCopy}
|
||||
@click=${this._handleCopyCard}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.cut"
|
||||
)}
|
||||
.path=${mdiContentCut}
|
||||
@click=${this._handleCutCard}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.delete"
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
@click=${this._handleDeleteCard}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
${keyed(
|
||||
this._getKey(this.cards, selected),
|
||||
html`<hui-card-element-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this.cards[selected]}
|
||||
.lovelace=${this.lovelace}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-card-element-editor>`
|
||||
)}
|
||||
`
|
||||
: html`
|
||||
<hui-card-picker
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this.lovelace}
|
||||
@config-changed=${this._handleCardPicked}
|
||||
></hui-card-picker>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getKey(cards: LovelaceCardConfig[], index: number): string {
|
||||
const key = `${index}-${cards.length}`;
|
||||
if (!this._keys.has(key)) {
|
||||
this._keys.set(key, Math.random().toString());
|
||||
}
|
||||
|
||||
return this._keys.get(key)!;
|
||||
}
|
||||
|
||||
private _handleAddCard() {
|
||||
this._selectedCard = this.cards.length;
|
||||
}
|
||||
|
||||
private _handleSelectedCard(ev) {
|
||||
this._guiMode = true;
|
||||
this._guiModeAvailable = true;
|
||||
this._selectedCard = parseInt(ev.detail.name, 10);
|
||||
}
|
||||
|
||||
private _handleConfigChanged(ev: HASSDomEvent<ConfigChangedEvent>) {
|
||||
ev.stopPropagation();
|
||||
const cards = [...this.cards];
|
||||
cards[this._selectedCard] = ev.detail.config as LovelaceCardConfig;
|
||||
this._fireCardsChanged(cards, ev.detail.guiModeAvailable);
|
||||
}
|
||||
|
||||
private _handleCardPicked(ev: HASSDomEvent<ConfigChangedEvent>) {
|
||||
ev.stopPropagation();
|
||||
this._keys.clear();
|
||||
this._fireCardsChanged([
|
||||
...this.cards,
|
||||
ev.detail.config as LovelaceCardConfig,
|
||||
]);
|
||||
}
|
||||
|
||||
private _handleCopyCard() {
|
||||
this._clipboard = deepClone(this.cards[this._selectedCard]);
|
||||
}
|
||||
|
||||
private _handleCutCard() {
|
||||
this._handleCopyCard();
|
||||
this._handleDeleteCard();
|
||||
}
|
||||
|
||||
private _handleDeleteCard() {
|
||||
const cards = [...this.cards];
|
||||
cards.splice(this._selectedCard, 1);
|
||||
this._selectedCard = Math.max(0, this._selectedCard - 1);
|
||||
this._keys.clear();
|
||||
this._fireCardsChanged(cards);
|
||||
}
|
||||
|
||||
private _handleMove(ev: Event) {
|
||||
const move = (ev.currentTarget as HTMLElement & { move: number }).move;
|
||||
const target = this._selectedCard + move;
|
||||
const cards = [...this.cards];
|
||||
const card = cards.splice(this._selectedCard, 1)[0];
|
||||
cards.splice(target, 0, card);
|
||||
this._selectedCard = target;
|
||||
this._keys.clear();
|
||||
this._fireCardsChanged(cards);
|
||||
}
|
||||
|
||||
private _handleGUIModeChanged(ev: HASSDomEvent<GUIModeChangedEvent>): void {
|
||||
ev.stopPropagation();
|
||||
this._guiMode = ev.detail.guiMode;
|
||||
this._guiModeAvailable = ev.detail.guiModeAvailable;
|
||||
}
|
||||
|
||||
private _toggleMode(): void {
|
||||
this._cardEditorEl?.toggleMode();
|
||||
}
|
||||
|
||||
private _fireCardsChanged(
|
||||
cards: LovelaceCardConfig[],
|
||||
guiModeAvailable?: boolean
|
||||
) {
|
||||
fireEvent(this, "cards-changed", { cards, guiModeAvailable });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
ha-tab-group {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
--ha-tab-track-color: var(--card-background-color);
|
||||
}
|
||||
.card-options {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
.editor {
|
||||
border: 1px solid var(--divider-color);
|
||||
padding: 12px;
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
.editor {
|
||||
margin: 0 -12px;
|
||||
}
|
||||
}
|
||||
.gui-mode-button {
|
||||
margin-right: auto;
|
||||
margin-inline-end: auto;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-card-list-editor": HuiCardListEditor;
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,10 @@ export class HuiEntityPickerTable extends LitElement {
|
||||
|
||||
const columns = this._columns(
|
||||
this.narrow,
|
||||
computeRTL(this.hass),
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
),
|
||||
showEntityId
|
||||
);
|
||||
|
||||
|
||||
@@ -177,7 +177,10 @@ export class HuiHeadingBadgesEditor extends LitElement {
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [entityName ? deviceName : undefined, areaName]
|
||||
|
||||
@@ -7,7 +7,6 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
import { NavigationPathInfoController } from "../../../../data/navigation-path-controller";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import { ServiceInfoController } from "../../../../data/service-info-controller";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { getShortcutCardDefaults } from "../../cards/hui-shortcut-card-defaults";
|
||||
@@ -23,7 +22,6 @@ const actions: UiAction[] = [
|
||||
"url",
|
||||
"assist",
|
||||
"perform-action",
|
||||
"show-popup",
|
||||
"none",
|
||||
];
|
||||
|
||||
@@ -46,8 +44,6 @@ export class HuiShortcutBadgeEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
||||
|
||||
@state() private _config?: ShortcutBadgeConfig;
|
||||
|
||||
private _navInfo = new NavigationPathInfoController(this);
|
||||
@@ -170,7 +166,6 @@ export class HuiShortcutBadgeEditor
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${this._schema(defaults.label, defaults.icon)}
|
||||
.context=${{ lovelace: this.lovelace }}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
import { NavigationPathInfoController } from "../../../../data/navigation-path-controller";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import { ServiceInfoController } from "../../../../data/service-info-controller";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { getShortcutCardDefaults } from "../../cards/hui-shortcut-card-defaults";
|
||||
@@ -24,7 +23,6 @@ const actions: UiAction[] = [
|
||||
"url",
|
||||
"assist",
|
||||
"perform-action",
|
||||
"show-popup",
|
||||
"none",
|
||||
];
|
||||
|
||||
@@ -49,8 +47,6 @@ export class HuiShortcutCardEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
||||
|
||||
@state() private _config?: ShortcutCardConfig;
|
||||
|
||||
private _navInfo = new NavigationPathInfoController(this);
|
||||
@@ -212,7 +208,6 @@ export class HuiShortcutCardEditor
|
||||
defaults.label,
|
||||
defaults.icon
|
||||
)}
|
||||
.context=${{ lovelace: this.lovelace }}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import {
|
||||
mdiCodeBraces,
|
||||
mdiContentCopy,
|
||||
mdiContentCut,
|
||||
mdiDelete,
|
||||
mdiListBoxOutline,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import {
|
||||
any,
|
||||
array,
|
||||
@@ -9,22 +20,29 @@ import {
|
||||
optional,
|
||||
string,
|
||||
} from "superstruct";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-icon-button-arrow-next";
|
||||
import "../../../../components/ha-icon-button-arrow-prev";
|
||||
import "../../../../components/ha-tab-group";
|
||||
import "../../../../components/ha-tab-group-tab";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { StackCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import "../card-editor/hui-card-list-editor";
|
||||
import type {
|
||||
CardsChangedEvent,
|
||||
HuiCardListEditor,
|
||||
} from "../card-editor/hui-card-list-editor";
|
||||
import "../card-editor/hui-card-element-editor";
|
||||
import type { HuiCardElementEditor } from "../card-editor/hui-card-element-editor";
|
||||
import "../card-editor/hui-card-picker";
|
||||
import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import type { GUIModeChangedEvent } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
@@ -51,12 +69,28 @@ export class HuiStackCardEditor
|
||||
|
||||
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
||||
|
||||
@storage({
|
||||
key: "dashboardCardClipboard",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
protected _clipboard?: LovelaceCardConfig;
|
||||
|
||||
@state() protected _config?: StackCardConfig;
|
||||
|
||||
@state() protected _selectedCard = 0;
|
||||
|
||||
@state() protected _GUImode = true;
|
||||
|
||||
@state() protected _guiModeAvailable? = true;
|
||||
|
||||
protected _keys = new Map<string, string>();
|
||||
|
||||
protected _schema: readonly HaFormSchema[] = SCHEMA;
|
||||
|
||||
@query("hui-card-list-editor")
|
||||
protected _cardListEditorEl?: HuiCardListEditor;
|
||||
@query("hui-card-element-editor")
|
||||
protected _cardEditorEl?: HuiCardElementEditor;
|
||||
|
||||
public setConfig(config: Readonly<StackCardConfig>): void {
|
||||
assert(config, cardConfigStruct);
|
||||
@@ -64,7 +98,7 @@ export class HuiStackCardEditor
|
||||
}
|
||||
|
||||
public focusYamlEditor() {
|
||||
this._cardListEditorEl?.focusYamlEditor();
|
||||
this._cardEditorEl?.focusYamlEditor();
|
||||
}
|
||||
|
||||
protected formData(): object {
|
||||
@@ -75,6 +109,11 @@ export class HuiStackCardEditor
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
const selected = this._selectedCard!;
|
||||
const numcards = this._config.cards.length;
|
||||
|
||||
const isGuiMode = !this._cardEditorEl || this._GUImode;
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
@@ -83,29 +122,202 @@ export class HuiStackCardEditor
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
<hui-card-list-editor
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this.lovelace}
|
||||
.cards=${this._config.cards}
|
||||
show-copy-cut
|
||||
@cards-changed=${this._cardsChanged}
|
||||
></hui-card-list-editor>
|
||||
<div class="card-config">
|
||||
<div class="toolbar">
|
||||
<ha-tab-group @wa-tab-show=${this._handleSelectedCard}>
|
||||
${this._config.cards.map(
|
||||
(_card, i) =>
|
||||
html`<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${i}
|
||||
.active=${i === selected}
|
||||
>
|
||||
${i + 1}
|
||||
</ha-tab-group-tab>`
|
||||
)}
|
||||
</ha-tab-group>
|
||||
<ha-icon-button
|
||||
@click=${this._handleAddCard}
|
||||
.path=${mdiPlus}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
|
||||
<div id="editor">
|
||||
${selected < numcards
|
||||
? html`
|
||||
<div id="card-options">
|
||||
<ha-icon-button
|
||||
class="gui-mode-button"
|
||||
@click=${this._toggleMode}
|
||||
.disabled=${!this._guiModeAvailable}
|
||||
.label=${this.hass!.localize(
|
||||
isGuiMode
|
||||
? "ui.panel.lovelace.editor.edit_card.show_code_editor"
|
||||
: "ui.panel.lovelace.editor.edit_card.show_visual_editor"
|
||||
)}
|
||||
.path=${isGuiMode ? mdiCodeBraces : mdiListBoxOutline}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-icon-button-arrow-prev
|
||||
.disabled=${selected === 0}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.move_before"
|
||||
)}
|
||||
@click=${this._handleMove}
|
||||
.move=${-1}
|
||||
></ha-icon-button-arrow-prev>
|
||||
|
||||
<ha-icon-button-arrow-next
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.move_after"
|
||||
)}
|
||||
.disabled=${selected === numcards - 1}
|
||||
@click=${this._handleMove}
|
||||
.move=${1}
|
||||
></ha-icon-button-arrow-next>
|
||||
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.copy"
|
||||
)}
|
||||
.path=${mdiContentCopy}
|
||||
@click=${this._handleCopyCard}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.cut"
|
||||
)}
|
||||
.path=${mdiContentCut}
|
||||
@click=${this._handleCutCard}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.delete"
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
@click=${this._handleDeleteCard}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
${keyed(
|
||||
this._getKey(this._config.cards, selected),
|
||||
html`<hui-card-element-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this._config.cards[selected]}
|
||||
.lovelace=${this.lovelace}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-card-element-editor>`
|
||||
)}
|
||||
`
|
||||
: html`
|
||||
<hui-card-picker
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this.lovelace}
|
||||
@config-changed=${this._handleCardPicked}
|
||||
></hui-card-picker>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected _cardsChanged(ev: HASSDomEvent<CardsChangedEvent>) {
|
||||
private _getKey(cards: LovelaceCardConfig[], index: number): string {
|
||||
const key = `${index}-${cards.length}`;
|
||||
if (!this._keys.has(key)) {
|
||||
this._keys.set(key, Math.random().toString());
|
||||
}
|
||||
|
||||
return this._keys.get(key)!;
|
||||
}
|
||||
|
||||
protected async _handleAddCard() {
|
||||
this._selectedCard = this._config!.cards.length;
|
||||
}
|
||||
|
||||
protected _handleSelectedCard(ev) {
|
||||
this._GUImode = true;
|
||||
this._guiModeAvailable = true;
|
||||
this._selectedCard = parseInt(ev.detail.name, 10);
|
||||
}
|
||||
|
||||
protected _handleConfigChanged(ev: HASSDomEvent<ConfigChangedEvent>) {
|
||||
ev.stopPropagation();
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
const cards = [...this._config.cards];
|
||||
const newCard = ev.detail.config as LovelaceCardConfig;
|
||||
cards[this._selectedCard] = newCard;
|
||||
this._config = { ...this._config, cards };
|
||||
this._guiModeAvailable = ev.detail.guiModeAvailable;
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
protected _handleCardPicked(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
const config = ev.detail.config;
|
||||
const cards = [...this._config.cards, config];
|
||||
this._config = { ...this._config, cards };
|
||||
this._keys.clear();
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
protected _handleCopyCard() {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
this._clipboard = deepClone(this._config.cards[this._selectedCard]);
|
||||
}
|
||||
|
||||
protected _handleCutCard() {
|
||||
this._handleCopyCard();
|
||||
this._handleDeleteCard();
|
||||
}
|
||||
|
||||
protected _handleDeleteCard() {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
const cards = [...this._config.cards];
|
||||
cards.splice(this._selectedCard, 1);
|
||||
this._config = { ...this._config, cards };
|
||||
this._selectedCard = Math.max(0, this._selectedCard - 1);
|
||||
this._keys.clear();
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
protected _handleMove(ev: Event) {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
const move = (ev.currentTarget as any).move;
|
||||
const source = this._selectedCard;
|
||||
const target = source + move;
|
||||
const cards = [...this._config.cards];
|
||||
const card = cards.splice(this._selectedCard, 1)[0];
|
||||
cards.splice(target, 0, card);
|
||||
this._config = {
|
||||
...this._config,
|
||||
cards: ev.detail.cards,
|
||||
cards,
|
||||
};
|
||||
fireEvent(this, "config-changed", {
|
||||
config: this._config,
|
||||
guiModeAvailable: ev.detail.guiModeAvailable,
|
||||
});
|
||||
this._selectedCard = target;
|
||||
this._keys.clear();
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
protected _handleGUIModeChanged(ev: HASSDomEvent<GUIModeChangedEvent>): void {
|
||||
ev.stopPropagation();
|
||||
this._GUImode = ev.detail.guiMode;
|
||||
this._guiModeAvailable = ev.detail.guiModeAvailable;
|
||||
}
|
||||
|
||||
protected _toggleMode(): void {
|
||||
this._cardEditorEl?.toggleMode();
|
||||
}
|
||||
|
||||
protected _valueChanged(ev: CustomEvent): void {
|
||||
@@ -117,7 +329,45 @@ export class HuiStackCardEditor
|
||||
`ui.panel.lovelace.editor.card.${this._config!.type}.${schema.name}`
|
||||
);
|
||||
|
||||
static styles = configElementStyle;
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
configElementStyle,
|
||||
css`
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
ha-tab-group {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
--ha-tab-track-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
#card-options {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#editor {
|
||||
border: 1px solid var(--divider-color);
|
||||
padding: 12px;
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
#editor {
|
||||
margin: 0 -12px;
|
||||
}
|
||||
}
|
||||
|
||||
.gui-mode-button {
|
||||
margin-right: auto;
|
||||
margin-inline-end: auto;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
array,
|
||||
any,
|
||||
boolean,
|
||||
dynamic,
|
||||
enums,
|
||||
@@ -62,14 +61,6 @@ const actionConfigStructAssist = type({
|
||||
start_listening: optional(boolean()),
|
||||
});
|
||||
|
||||
const actionConfigStructShowPopup = object({
|
||||
action: literal("show-popup"),
|
||||
desktop_mode: optional(enums(["popover", "dialog"])),
|
||||
mobile_mode: optional(enums(["bottom-sheet", "dialog"])),
|
||||
cards: array(any()),
|
||||
confirmation: optional(actionConfigStructConfirmation),
|
||||
});
|
||||
|
||||
const actionConfigStructMoreInfo = type({
|
||||
action: literal("more-info"),
|
||||
entity: optional(string()),
|
||||
@@ -85,7 +76,6 @@ export const actionConfigStructType = object({
|
||||
"url",
|
||||
"navigate",
|
||||
"assist",
|
||||
"show-popup",
|
||||
]),
|
||||
confirmation: optional(actionConfigStructConfirmation),
|
||||
});
|
||||
@@ -108,9 +98,6 @@ export const actionConfigStruct = dynamic<any>((value) => {
|
||||
case "assist": {
|
||||
return actionConfigStructAssist;
|
||||
}
|
||||
case "show-popup": {
|
||||
return actionConfigStructShowPopup;
|
||||
}
|
||||
case "more-info": {
|
||||
return actionConfigStructMoreInfo;
|
||||
}
|
||||
|
||||
@@ -133,7 +133,10 @@ export class HuiViewHeaderSettingsEditor extends LitElement {
|
||||
};
|
||||
|
||||
const narrow = this.narrow;
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
const schema = this._schema(this.hass.localize, isRTL, narrow);
|
||||
|
||||
return html`
|
||||
|
||||
@@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-date-input";
|
||||
import "../../../components/ha-time-input";
|
||||
import { isUnavailableState, UNKNOWN } from "../../../data/entity/entity";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
|
||||
import {
|
||||
setInputDateTimeValue,
|
||||
stateToIsoDateString,
|
||||
@@ -65,7 +65,7 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
|
||||
<ha-date-input
|
||||
.label=${stateObj.attributes.has_time ? name : undefined}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
.value=${stateToIsoDateString(stateObj)}
|
||||
@value-changed=${this._dateChanged}
|
||||
>
|
||||
@@ -81,7 +81,7 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
|
||||
? stateObj.state.split(" ")[1]
|
||||
: stateObj.state}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
@value-changed=${this._timeChanged}
|
||||
@click=${this._stopEventPropagation}
|
||||
></ha-time-input>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-slider";
|
||||
import "../../../components/input/ha-input";
|
||||
import { isUnavailableState } from "../../../data/entity/entity";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import { setValue } from "../../../data/input_text";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
@@ -78,7 +78,7 @@ class HuiInputNumberEntityRow extends LitElement implements LovelaceRow {
|
||||
<div class="flex">
|
||||
<ha-slider
|
||||
labeled
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
.step=${Number(stateObj.attributes.step)}
|
||||
.min=${Number(stateObj.attributes.min)}
|
||||
.max=${Number(stateObj.attributes.max)}
|
||||
@@ -93,7 +93,7 @@ class HuiInputNumberEntityRow extends LitElement implements LovelaceRow {
|
||||
: html`
|
||||
<div class="flex state">
|
||||
<ha-input
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
pattern="[0-9]+([\\.][0-9]+)?"
|
||||
.step=${Number(stateObj.attributes.step)}
|
||||
.min=${Number(stateObj.attributes.min)}
|
||||
|
||||
@@ -141,7 +141,6 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
|
||||
<ha-state-icon
|
||||
class="weather-icon"
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
`}
|
||||
</div>
|
||||
|
||||
@@ -170,7 +170,6 @@ export class HuiEntityHeadingBadge
|
||||
? html`
|
||||
<ha-state-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.icon=${config.icon}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-icon>
|
||||
|
||||
@@ -70,11 +70,7 @@ export class HuiButtonRow extends LitElement implements LovelaceRow {
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-state-icon
|
||||
.icon=${this._config.icon}
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
<ha-state-icon .icon=${this._config.icon} .stateObj=${stateObj}>
|
||||
</ha-state-icon>
|
||||
<div class="flex">
|
||||
<div .title=${name}>${name}</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user