Compare commits

...

18 Commits

Author SHA1 Message Date
Wendelin 8af5908682 Fix add T/C/A floor auto open; Target details adaptive dialog. (#52001)
* Auto open single floor

* Use adaptive dialog for target details

* review
2026-05-12 19:28:24 +03:00
George Caliment 60e95b886c Fixed how ha-entity-toggle sets ha-switch styles var (#51984) 2026-05-12 16:46:01 +02:00
Wendelin 0385ca8076 Add link to single integration entry warning (#51977)
* Add link to single integration entry warning

* Refactor single config entry warning: move function to dedicated file and update imports

* Implement single config entry warning dialog and update related functions

* Apply suggestions from code review

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-05-12 10:33:02 +00:00
Tom Carpenter 02c65fc8cb Position bars on statistics charts at centre of data point time range (#51957)
* Position statistics chart bars at centre of time range

When displaying 5minute or hourly data periods, position each bar at the midpoint of its start/end time. This mimics the behaviour in the various energy cards for consistency.

* Move limit comparison into pushData
Results in clearer function argument usage.

* Add time range for statistics-chart bar tooltip

When using hour/5minute periods the bars are recentred. Update the tooltips to show time range they cover.

* Omit time from tooltip for bars with periods of day or longer

Don't clutter the tooltip with unnecessary times of 0:00 when using day/month/year timescales on bar charts, just show the date range.

For week/month/year, we now also include the range of dates of the bar rather than just the start date.
2026-05-12 12:33:39 +03:00
Wendelin 49290d5c83 Add macOS version mapping for Safari 26 support (#51999) 2026-05-12 09:26:04 +02:00
Jan-Philipp Benecke 08aff3bfd7 Replace variable display in trace view with ha-code-editor (#51997)
* Replace variable display in trace view with ha-code-editor

* Replace variable display in trace view with ha-code-editor
2026-05-12 09:13:52 +03:00
Petar Petrov 455fa45b9c Show battery state of charge on the energy distribution card (#51812)
* Show battery state of charge on the energy distribution card

* css tweak

* Only show SOC-based battery icon when the period includes now
2026-05-12 08:38:04 +03:00
karwosts 2e56a4ec4c fix spurious timeline-chart exceptions (#51996) 2026-05-12 08:13:07 +03:00
Copilot 76131ff09e Hide standalone helpers and entities from the Home “Other devices” view (#51853)
* Initial plan

* Hide standalone helpers and entities from other devices view

Agent-Logs-Url: https://github.com/home-assistant/frontend/sessions/ecad0a9d-6983-4c5c-9728-6ef04be88e42

* Simplify other devices strategy test assertions

Agent-Logs-Url: https://github.com/home-assistant/frontend/sessions/ecad0a9d-6983-4c5c-9728-6ef04be88e42

* Clean up other devices strategy test helpers

Agent-Logs-Url: https://github.com/home-assistant/frontend/sessions/ecad0a9d-6983-4c5c-9728-6ef04be88e42

* Polish other devices strategy test fixtures

Agent-Logs-Url: https://github.com/home-assistant/frontend/sessions/ecad0a9d-6983-4c5c-9728-6ef04be88e42

* Remove other devices strategy test file

Agent-Logs-Url: https://github.com/home-assistant/frontend/sessions/2a54dac8-a7fc-42e5-a309-e0af02ca4303

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-11 20:27:00 +02:00
Wendelin 89d8723c5a Fix dialog expose entity in firefox (#51974)
* Migrate dialog-expose-entity to new dialog and migrate everything thats needed for this.

* Load virtualizer after dialog show is ready

* Use entities context instead of registries in ha-state-icon

* fix types

* Update src/panels/config/voice-assistants/dialog-expose-entity.ts

---------

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2026-05-11 18:13:45 +00:00
renovate[bot] 7bdb63a6fe Update dependency terser-webpack-plugin to v5.6.0 (#51992)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 17:58:16 +00:00
Jan-Philipp Benecke eed79f1797 Use ha-tab-group for in automation/script trace page (#51991) 2026-05-11 19:50:10 +02:00
Joakim Plate 76665009da Let input entities date and number be active when unknown (#29306)
Let input of date and number be active when unknown
2026-05-11 17:54:40 +02:00
renovate[bot] 6d7d08fddc Update dependency lint-staged to v17.0.3 (#51985)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 17:52:39 +02:00
Alex van den Hoogen 77d4e6dc43 Fixes tile card misalignment (#25745) (#51964)
* Fixes tile card misalignment (#25745)

Removes an unnecessary vertical padding on the tile card content that causes a misalignment within the Android Companion app. This padding isn't needed because the contents are already vertically aligned with flexbox anyway.

* Added a min-height to tile container

As requested in the review, added a minimal height to the content of the
tile container to support non-section layouts.
2026-05-11 17:33:47 +02:00
Wendelin 7345256b30 Fix ha list ha sidebar (#51979)
* Fix ha-list in ha-sidebar

* Fix ha-row-item start/end slots
2026-05-11 16:34:37 +02:00
renovate[bot] e0d98e95fa Update dependency @lokalise/node-api to v16 (#51983)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 15:57:40 +03:00
renovate[bot] 17041044cf Update dependency @rsdoctor/rspack-plugin to v1.5.10 (#51982)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 15:14:55 +03:00
101 changed files with 972 additions and 640 deletions
+2
View File
@@ -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
View File
@@ -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",
+38
View File
@@ -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 =
+10 -6
View File
@@ -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",
+108 -31
View File
@@ -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);
}
});
+11 -3
View File
@@ -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
+8 -2
View File
@@ -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]
-1
View File
@@ -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)}
+4 -2
View File
@@ -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
+6 -1
View File
@@ -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 = {
+1 -5
View File
@@ -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>`;
+4 -1
View File
@@ -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,
})}
>
-5
View File
@@ -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>
+6 -1
View File
@@ -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"
+10 -6
View File
@@ -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>
+11 -16
View File
@@ -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 {
+32 -13
View File
@@ -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)}`;
}
+4 -1
View File
@@ -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;
}
+30 -9
View File
@@ -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 {
-1
View File
@@ -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}
+2 -1
View File
@@ -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;
+27 -13
View File
@@ -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;
}
`,
];
}
-40
View File
@@ -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);
}
`;
+1
View File
@@ -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";
+4 -1
View File
@@ -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
View File
@@ -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) {
@@ -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>
+4 -1
View File
@@ -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
+1 -2
View File
@@ -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
)
? " ◂ "
: " ▸ "
);
}
}
@@ -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: {
+4 -1
View File
@@ -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:
+37 -58
View File
@@ -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>
@@ -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">
+6 -9
View File
@@ -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,
+3 -4
View File
@@ -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>
@@ -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>
@@ -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 &&
@@ -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]
@@ -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>
@@ -1,7 +1,6 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
import {
findEntities,
@@ -11,7 +10,6 @@ import { clamp } from "../../../../common/number/clamp";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import { isHelperDomain } from "../../../config/helpers/const";
import type {
EmptyStateCardConfig,
EntitiesCardConfig,
@@ -41,7 +39,6 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement {
const sections: LovelaceSectionRawConfig[] = [];
const entitiesByDevice: Record<string, string[]> = {};
const entitiesWithoutDevices: string[] = [];
for (const entityId of otherDevicesEntities) {
const stateObj = hass.states[entityId];
if (!stateObj) continue;
@@ -53,7 +50,6 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement {
hass.floors
);
if (!device) {
entitiesWithoutDevices.push(entityId);
continue;
}
if (!(device.id in entitiesByDevice)) {
@@ -69,16 +65,6 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement {
})
);
const helpersEntities = entitiesWithoutDevices.filter((entityId) => {
const domain = computeDomain(entityId);
return isHelperDomain(domain);
});
const otherEntities = entitiesWithoutDevices.filter((entityId) => {
const domain = computeDomain(entityId);
return !isHelperDomain(domain);
});
const primaryFilter = generateEntityFilter(hass, {
entity_category: "none",
});
@@ -149,44 +135,6 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement {
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
const maxColumns = clamp(sections.length, 2, 3);
if (helpersEntities.length) {
sections.push({
type: "grid",
column_span: maxColumns,
cards: [
{
type: "heading",
heading: hass.localize(
"ui.panel.lovelace.strategy.home-other-devices.helpers"
),
} satisfies HeadingCardConfig,
...helpersEntities.map((e) => ({
type: "tile",
entity: e,
})),
],
});
}
if (otherEntities.length) {
sections.push({
type: "grid",
column_span: maxColumns,
cards: [
{
type: "heading",
heading: hass.localize(
"ui.panel.lovelace.strategy.home-other-devices.entities"
),
} satisfies HeadingCardConfig,
...otherEntities.map((e) => ({
type: "tile",
entity: e,
})),
],
});
}
// No sections, show empty state
if (sections.length === 0) {
return {
+4 -1
View File
@@ -78,7 +78,10 @@ export class PanelView extends LitElement implements LovelaceViewElement {
size="large"
@click=${this._addCard}
class=${classMap({
rtl: computeRTL(this.hass!),
rtl: computeRTL(
this.hass!.language,
this.hass!.translationMetadata.translations
),
})}
>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
@@ -27,9 +27,11 @@ import "../../components/ha-domain-icon";
import "../../components/ha-dropdown";
import "../../components/ha-icon-button";
import "../../components/ha-slider";
import type { HaSlider } from "../../components/ha-slider";
import "../../components/ha-spinner";
import "../../components/ha-state-icon";
import "../../components/ha-svg-icon";
import "../../components/media-player/ha-media-player-picker";
import type {
ControlButton,
MediaPlayerEntity,
@@ -50,13 +52,11 @@ import type { ResolvedMediaSource } from "../../data/media_source";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../types";
import type { HaSlider } from "../../components/ha-slider";
import "../lovelace/components/hui-marquee";
import {
BrowserMediaPlayer,
ERR_UNSUPPORTED_MEDIA,
} from "./browser-media-player";
import "../../components/media-player/ha-media-player-picker";
declare global {
interface HASSDomEvents {
@@ -417,9 +417,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
return html`<ha-svg-icon .path=${mdiMonitor}></ha-svg-icon>`;
}
if (stateObj) {
return html`
<ha-state-icon .hass=${this.hass} .stateObj=${stateObj}></ha-state-icon>
`;
return html` <ha-state-icon .stateObj=${stateObj}></ha-state-icon> `;
}
return html`
<ha-domain-icon .domain=${computeDomain(this.entityId)}></ha-domain-icon>
+1 -5
View File
@@ -174,11 +174,7 @@ class PanelTodo extends LitElement {
value=${list.entity_id}
.selected=${list.entity_id === this._entityId}
>
<ha-state-icon
.stateObj=${list}
.hass=${this.hass}
slot="icon"
></ha-state-icon
<ha-state-icon .stateObj=${list} slot="icon"></ha-state-icon
>${list.name}
</ha-dropdown-item> `
);
@@ -80,7 +80,6 @@ export class HaStateControlCoverToggle extends LitElement {
})}
>
<ha-state-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
stateValue="open"
></ha-state-icon>
@@ -97,7 +96,6 @@ export class HaStateControlCoverToggle extends LitElement {
})}
>
<ha-state-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
stateValue="closed"
></ha-state-icon>
@@ -124,13 +122,11 @@ export class HaStateControlCoverToggle extends LitElement {
>
<ha-state-icon
slot="icon-on"
.hass=${this.hass}
.stateObj=${this.stateObj}
stateValue="open"
></ha-state-icon>
<ha-state-icon
slot="icon-off"
.hass=${this.hass}
.stateObj=${this.stateObj}
stateValue="closed"
></ha-state-icon>
@@ -96,7 +96,6 @@ export class HaStateControlLockToggle extends LitElement {
@click=${this._turnOn}
>
<ha-state-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateValue=${locking ? "locking" : "locked"}
></ha-state-icon>
@@ -106,7 +105,6 @@ export class HaStateControlLockToggle extends LitElement {
@click=${this._turnOff}
>
<ha-state-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateValue=${unlocking ? "unlocking" : "unlocked"}
></ha-state-icon>
@@ -133,14 +131,12 @@ export class HaStateControlLockToggle extends LitElement {
>
<ha-state-icon
slot="icon-on"
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateValue=${locking ? "locking" : "locked"}
class=${classMap({ pulse: locking })}
></ha-state-icon>
<ha-state-icon
slot="icon-off"
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateValue=${unlocking ? "unlocking" : "unlocked"}
class=${classMap({ pulse: unlocking })}
@@ -80,7 +80,6 @@ export class HaStateControlValveToggle extends LitElement {
})}
>
<ha-state-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
stateValue="open"
></ha-state-icon>
@@ -97,7 +96,6 @@ export class HaStateControlValveToggle extends LitElement {
})}
>
<ha-state-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
stateValue="closed"
></ha-state-icon>
@@ -124,13 +122,11 @@ export class HaStateControlValveToggle extends LitElement {
>
<ha-state-icon
slot="icon-on"
.hass=${this.hass}
.stateObj=${this.stateObj}
stateValue="open"
></ha-state-icon>
<ha-state-icon
slot="icon-off"
.hass=${this.hass}
.stateObj=${this.stateObj}
stateValue="closed"
></ha-state-icon>
+3 -3
View File
@@ -6,7 +6,7 @@ import { debounce } from "../common/util/debounce";
import "../components/entity/state-info";
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";
@@ -57,7 +57,7 @@ class StateCardInputNumber extends LitElement {
<div class="flex">
<ha-slider
labeled
.disabled=${isUnavailableState(this.stateObj.state)}
.disabled=${this.stateObj.state === UNAVAILABLE}
.step=${Number(this.stateObj.attributes.step)}
.min=${Number(this.stateObj.attributes.min)}
.max=${Number(this.stateObj.attributes.max)}
@@ -72,7 +72,7 @@ class StateCardInputNumber extends LitElement {
: html`
<div class="flex state">
<ha-input
.disabled=${isUnavailableState(this.stateObj.state)}
.disabled=${this.stateObj.state === UNAVAILABLE}
pattern="[0-9]+([\\.][0-9]+)?"
.step=${Number(this.stateObj.attributes.step)}
.min=${Number(this.stateObj.attributes.min)}
+3 -3
View File
@@ -6,7 +6,7 @@ import { debounce } from "../common/util/debounce";
import "../components/entity/state-info";
import "../components/ha-slider";
import "../components/input/ha-input";
import { isUnavailableState } from "../data/entity/entity";
import { UNAVAILABLE } from "../data/entity/entity";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
@@ -60,7 +60,7 @@ class StateCardNumber extends LitElement {
<div class="flex">
<ha-slider
labeled
.disabled=${isUnavailableState(this.stateObj.state)}
.disabled=${this.stateObj.state === UNAVAILABLE}
.step=${Number(this.stateObj.attributes.step)}
.min=${Number(this.stateObj.attributes.min)}
.max=${Number(this.stateObj.attributes.max)}
@@ -75,7 +75,7 @@ class StateCardNumber extends LitElement {
`
: html` <div class="flex state">
<ha-input
.disabled=${isUnavailableState(this.stateObj.state)}
.disabled=${this.stateObj.state === UNAVAILABLE}
pattern="[0-9]+([\\.][0-9]+)?"
.step=${Number(this.stateObj.attributes.step)}
.min=${Number(this.stateObj.attributes.min)}
+3
View File
@@ -4185,6 +4185,8 @@
"energy_helper_out": "Pick a sensor that measures the electricity flowing out of the battery in either of {unit}.",
"energy_into_battery": "Energy charged into the battery",
"energy_out_of_battery": "Energy discharged from the battery",
"state_of_charge": "Battery state of charge sensor",
"state_of_charge_helper": "Sensor reporting battery state of charge as %.",
"power": "Battery power",
"power_helper": "Pick a sensor which measures the electricity flowing into and out of the battery in either of {unit}. Positive values indicate discharging the battery, negative values indicate charging the battery.",
"sensor_type": "Type of power measurement",
@@ -6908,6 +6910,7 @@
"supported_hardware": "supported hardware",
"proceed": "Proceed",
"single_config_entry_title": "This integration allows only one configuration",
"show_integration": "Show integration",
"single_config_entry": "{integration_name} supports only one configuration. Adding additional ones is not needed."
}
},
+31 -1
View File
@@ -1,8 +1,17 @@
import {
mdiBattery,
mdiBattery10,
mdiBattery50,
mdiBattery90,
mdiBatteryAlertVariantOutline,
mdiBatteryUnknown,
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { describe, it, expect } from "vitest";
import { describe, expect, it } from "vitest";
import {
batteryIcon,
batteryLevelIcon,
batteryLevelIconPath,
} from "../../../src/common/entity/battery_icon";
describe("batteryIcon", () => {
@@ -43,3 +52,24 @@ describe("batteryLevelIcon", () => {
expect(batteryLevelIcon("on")).toBe("mdi:battery-alert");
});
});
describe("batteryLevelIconPath", () => {
it("rounds to the nearest 10% bucket", () => {
expect(batteryLevelIconPath(46)).toBe(mdiBattery50);
expect(batteryLevelIconPath(94)).toBe(mdiBattery90);
expect(batteryLevelIconPath(95)).toBe(mdiBattery);
});
it("returns the alert path for very low levels", () => {
expect(batteryLevelIconPath(0)).toBe(mdiBatteryAlertVariantOutline);
expect(batteryLevelIconPath(5)).toBe(mdiBatteryAlertVariantOutline);
});
it("returns the 10% bucket just above the alert threshold", () => {
expect(batteryLevelIconPath(6)).toBe(mdiBattery10);
});
it("returns the unknown path for non-numeric input", () => {
expect(batteryLevelIconPath("unavailable")).toBe(mdiBatteryUnknown);
});
});

Some files were not shown because too many files have changed in this diff Show More