Compare commits

...

13 Commits

Author SHA1 Message Date
pcan08 0d545d744b Remove dead diagnostics code from integration card (#52606)
supportsDiagnostics and _diagnosticHandlers were no longer used in the
card template.
2026-06-14 22:03:19 +02:00
Petar Petrov f39dab2de5 Remove misleading "Total exported" line from energy usage tooltip (#52605) 2026-06-14 22:02:54 +02:00
Arsène Reymond 1527117015 fix: font-family for breadcrumb & select-anchor (#52612) 2026-06-14 21:55:32 +02:00
Franck Nijhof 26794560ac Remove unused emptyImageBase64 helper (#52614)
The emptyImageBase64 constant in src/common/empty_image_base64.ts has no
references anywhere in the codebase.
2026-06-14 21:54:45 +02:00
Franck Nijhof 976f9de8ad Remove unused timezone-datalist component (#52615)
The createTimezoneListEl helper in src/components/timezone-datalist.ts has
no references anywhere in the codebase. The google-timezones-json
dependency it used is still imported by other modules, so it is kept.
2026-06-14 21:54:23 +02:00
Franck Nijhof 6810bc5412 Remove unused scrollToTarget helper (#52616)
The default-exported scrollToTarget function in
src/common/dom/scroll-to-target.ts (a legacy copy from
paper-scroll-header-panel) has no references anywhere in the codebase.
2026-06-14 21:53:43 +02:00
Franck Nijhof a4ca54b80b Remove unused loadImg helper (#52617)
The loadImg export in load_resource.ts is not referenced anywhere; drop it
and narrow the internal _load tag type to the used values.
2026-06-14 21:52:59 +02:00
Franck Nijhof 07f0ef0ded Remove unused light helpers (#52619)
lightIsInColorMode and formatTempColor in data/light.ts are not referenced
anywhere in the codebase.
2026-06-14 21:52:25 +02:00
Franck Nijhof cf89bb32ab Remove unused replaceTileLayer helper (#52618)
Remove unused replaceTileLayer and LeafletDrawModuleType

Neither the replaceTileLayer helper nor the LeafletDrawModuleType type in
setup-leaflet-map.ts is referenced anywhere in the codebase.
2026-06-14 21:51:54 +02:00
Franck Nijhof ec5cbd16d8 Add accessible labels to entity ID copy/restore buttons (#52620)
The copy and restore icon buttons next to the entity ID field in the
entity settings dialog had no accessible name. Add descriptive labels
using two new translation keys.
2026-06-14 21:51:05 +02:00
Franck Nijhof 926abd7fc5 Replace Latin "e.g." with plain English in translations (#52621) 2026-06-14 21:50:27 +02:00
Franck Nijhof e227bbe9a2 Add tests for isTimestamp string utility (#52622) 2026-06-14 21:49:46 +02:00
Franck Nijhof f82b0b61e5 Add accessible labels to automation editor icon buttons (#52613)
Icon-only ha-icon-buttons have no accessible name, so screen readers
announce nothing. Add labels (using existing translation keys) to the
conversation trigger's add/remove sentence buttons and the integration
documentation buttons in the trigger and condition platform editors.
2026-06-14 15:21:24 +02:00
18 changed files with 77 additions and 127 deletions
+1 -2
View File
@@ -1,7 +1,7 @@
// Load a resource and get a promise when loading done.
// From: https://davidwalsh.name/javascript-loader
const _load = (tag: "link" | "script" | "img", url: string, type?: "module") =>
const _load = (tag: "link" | "script", url: string, type?: "module") =>
// This promise will be used by Promise.all to determine success or failure
new Promise((resolve, reject) => {
const element = document.createElement(tag);
@@ -33,5 +33,4 @@ const _load = (tag: "link" | "script" | "img", url: string, type?: "module") =>
});
export const loadCSS = (url: string) => _load("link", url);
export const loadJS = (url: string) => _load("script", url);
export const loadImg = (url: string) => _load("img", url);
export const loadModule = (url: string) => _load("script", url, "module");
-41
View File
@@ -1,41 +0,0 @@
/**
* Scroll to a specific y coordinate.
*
* Copied from paper-scroll-header-panel.
*
* @method scroll
* @param {number} top The coordinate to scroll to, along the y-axis.
* @param {boolean} smooth true if the scroll position should be smoothly adjusted.
*/
export default function scrollToTarget(element, target) {
// the scroll event will trigger _updateScrollState directly,
// However, _updateScrollState relies on the previous `scrollTop` to update the states.
// Calling _updateScrollState will ensure that the states are synced correctly.
const top = 0;
const scroller = target;
const easingFn = function easeOutQuad(t, b, c, d) {
t /= d;
return -c * t * (t - 2) + b;
};
const animationId = Math.random();
const duration = 200;
const startTime = Date.now();
const currentScrollTop = scroller.scrollTop;
const deltaScrollTop = top - currentScrollTop;
element._currentAnimationId = animationId;
(function updateFrame() {
const now = Date.now();
const elapsedTime = now - startTime;
if (elapsedTime > duration) {
scroller.scrollTop = top;
} else if (element._currentAnimationId === animationId) {
scroller.scrollTop = easingFn(
elapsedTime,
currentScrollTop,
deltaScrollTop,
duration
);
requestAnimationFrame(updateFrame.bind(element));
}
}).call(element);
}
-13
View File
@@ -3,8 +3,6 @@ import type { Map, TileLayer } from "leaflet";
// Sets up a Leaflet map on the provided DOM element
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
export type LeafletModuleType = typeof import("leaflet");
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
export type LeafletDrawModuleType = typeof import("leaflet-draw");
export const setupLeafletMap = async (
mapElement: HTMLElement,
@@ -45,17 +43,6 @@ export const setupLeafletMap = async (
return [map, Leaflet, tileLayer];
};
export const replaceTileLayer = (
leaflet: LeafletModuleType,
map: Map,
tileLayer: TileLayer
): TileLayer => {
map.removeLayer(tileLayer);
tileLayer = createTileLayer(leaflet);
tileLayer.addTo(map);
return tileLayer;
};
const createTileLayer = (leaflet: LeafletModuleType): TileLayer =>
leaflet.tileLayer(
`https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}${
-3
View File
@@ -1,3 +0,0 @@
/** An empty image which can be set as src of an img element. */
export const emptyImageBase64 =
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
+1
View File
@@ -183,6 +183,7 @@ export class HaControlSelectMenu extends LitElement {
gap: 10px;
width: 100%;
user-select: none;
font-family: var(--ha-font-family-body, inherit);
font-style: normal;
font-weight: var(--ha-font-weight-normal);
letter-spacing: 0.25px;
-13
View File
@@ -1,13 +0,0 @@
import timezones from "google-timezones-json";
export const createTimezoneListEl = () => {
const list = document.createElement("datalist");
list.id = "timezones";
Object.keys(timezones).forEach((key) => {
const option = document.createElement("option");
option.value = key;
option.innerText = timezones[key];
list.appendChild(option);
});
return list;
};
-7
View File
@@ -43,11 +43,6 @@ export const lightSupportsColorMode = (
mode: LightColorMode
) => entity.attributes.supported_color_modes?.includes(mode) || false;
export const lightIsInColorMode = (entity: LightEntity) =>
(entity.attributes.color_mode &&
modesSupportingColor.includes(entity.attributes.color_mode)) ||
false;
export const lightSupportsColor = (entity: LightEntity) =>
entity.attributes.supported_color_modes?.some((mode) =>
modesSupportingColor.includes(mode)
@@ -159,5 +154,3 @@ export const computeDefaultFavoriteColors = (
return colors;
};
export const formatTempColor = (value: number) => `${value} K`;
@@ -1103,6 +1103,7 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
.title .breadcrumb {
color: var(--secondary-text-color);
font-size: var(--ha-font-size-m);
font-family: var(--ha-font-family-heading, inherit);
line-height: 16px;
--mdc-icon-size: 16px;
padding: var(--ha-space-1);
@@ -164,6 +164,9 @@ export class HaPlatformCondition extends LitElement {
<ha-icon-button
.path=${mdiHelpCircleOutline}
class="help-icon"
.label=${this.hass.localize(
"ui.components.service-control.integration_doc"
)}
></ha-icon-button>
</a>`
: nothing}
@@ -55,6 +55,9 @@ export class HaConversationTrigger
@click=${this._removeOption}
slot="end"
.path=${mdiClose}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.conversation.delete"
)}
></ha-icon-button>
</ha-input>
`
@@ -78,6 +81,9 @@ export class HaConversationTrigger
@click=${this._addOption}
slot="end"
.path=${mdiPlus}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.conversation.add_sentence"
)}
></ha-icon-button>
</ha-input>`;
}
@@ -201,6 +201,9 @@ export class HaPlatformTrigger extends LitElement {
<ha-icon-button
.path=${mdiHelpCircleOutline}
class="help-icon"
.label=${this.hass.localize(
"ui.components.service-control.integration_doc"
)}
></ha-icon-button>
</a>`
: nothing}
@@ -894,11 +894,17 @@ export class EntityRegistrySettingsEditor extends LitElement {
slot="end"
@click=${this._restoreEntityId}
.path=${mdiRestore}
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.restore_entity_id"
)}
></ha-icon-button>
<ha-icon-button
slot="end"
@click=${this._copyEntityId}
.path=${mdiContentCopy}
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.copy_entity_id"
)}
></ha-icon-button>
</ha-input>
${!this.entry.device_id
@@ -27,7 +27,6 @@ import "../../../components/input/ha-input-search";
import type { HaInputSearch } from "../../../components/input/ha-input-search";
import type { ConfigEntry } from "../../../data/config_entries";
import { getConfigEntries } from "../../../data/config_entries";
import { fetchDiagnosticHandlers } from "../../../data/diagnostics";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import { subscribeEntityRegistry } from "../../../data/entity/entity_registry";
import { fetchEntitySourcesWithCache } from "../../../data/entity/entity_sources";
@@ -163,8 +162,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
@state() private _filter: string = history.state?.filter || "";
@state() private _diagnosticHandlers?: Record<string, boolean>;
@state() private _logInfos?: Record<string, IntegrationLogInfo>;
@query("ha-input-search") private _searchInput!: HaInputSearch;
@@ -386,16 +383,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
this._handleRouteChanged();
this._scanUSBDevices();
this._scanImprovDevices();
if (isComponentLoaded(this.hass.config, "diagnostics")) {
fetchDiagnosticHandlers(this.hass).then((infos) => {
const handlers = {};
for (const info of infos) {
handlers[info.domain] = info.handlers.config_entry;
}
this._diagnosticHandlers = handlers;
});
}
}
protected updated(changed: PropertyValues<this>) {
@@ -650,9 +637,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
.manifest=${this._manifests[domain]}
.entityRegistryEntries=${this._entityRegistryEntries}
.domainEntities=${this._domainEntities[domain] || []}
.supportsDiagnostics=${this._diagnosticHandlers
? this._diagnosticHandlers[domain]
: false}
.logInfo=${this._logInfos
? this._logInfos[domain]
: nothing}
@@ -38,9 +38,6 @@ export class HaIntegrationCard extends LitElement {
@property({ attribute: false })
public entityRegistryEntries!: EntityRegistryEntry[];
@property({ attribute: "supports-diagnostics", type: Boolean })
public supportsDiagnostics = false;
@property({ attribute: false }) public logInfo?: IntegrationLogInfo;
@property({ attribute: false }) public domainEntities: string[] = [];
@@ -267,8 +267,6 @@ function formatTooltip(
let sumPositive = 0;
let countPositive = 0;
let sumNegative = 0;
let countNegative = 0;
const rows: TemplateResult[] = [];
for (const param of params) {
const y = param.value?.[1] as number;
@@ -280,14 +278,12 @@ function formatTooltip(
if (value === "0") {
continue;
}
if (param.componentSubType === "bar") {
if (y > 0) {
sumPositive += y;
countPositive++;
} else {
sumNegative += y;
countNegative++;
}
// Only the positive bars (consumption) are summed into a total. Negative
// bars mix unrelated categories (grid export and battery charge), so they
// are not totaled.
if (param.componentSubType === "bar" && y > 0) {
sumPositive += y;
countPositive++;
}
rows.push(
html`<ha-chart-tooltip-marker
@@ -305,8 +301,6 @@ function formatTooltip(
(row, i) => html`${i > 0 ? html`<br />` : nothing}${row}`
)}${sumPositive !== 0 && countPositive > 1 && formatTotal
? html`<br /><b>${formatTotal(sumPositive)}</b>`
: nothing}${sumNegative !== 0 && countNegative > 1 && formatTotal
? html`<br /><b>${formatTotal(sumNegative)}</b>`
: nothing}`;
}
@@ -181,15 +181,10 @@ export class HuiEnergyUsageGraphCard
}
private _formatTotal = (total: number) =>
total > 0
? this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_usage_graph.total_consumed",
{ num: formatNumber(total, this.hass.locale) }
)
: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_usage_graph.total_returned",
{ num: formatNumber(-total, this.hass.locale) }
);
this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_usage_graph.total_consumed",
{ num: formatNumber(total, this.hass.locale) }
);
private _createOptions = memoizeOne(
(
+9 -8
View File
@@ -600,7 +600,7 @@
}
},
"template": {
"yaml_warning": "It appears you may be writing YAML into this template field (saw ''{string}''), which is likely incorrect. This field is intended for templates only (e.g. '{{ states(sensor.test) > 0 }}' ).",
"yaml_warning": "It appears you may be writing YAML into this template field (saw ''{string}''), which is likely incorrect. This field is intended for templates only (for example, '{{ states(sensor.test) > 0 }}').",
"learn_more": "Learn more about templating"
},
"text": {
@@ -1892,12 +1892,14 @@
"editor": {
"name": "Name",
"icon": "Icon",
"icon_error": "Icons should be in the format 'prefix:iconname', e.g. 'mdi:home'",
"icon_error": "Icons should be in the format 'prefix:iconname', like 'mdi:home'",
"default_code": "Default code",
"default_code_error": "Code does not match code format",
"calendar_color": "Calendar color",
"associated_zone": "Associated zone",
"entity_id": "Entity ID",
"copy_entity_id": "Copy entity ID",
"restore_entity_id": "Restore entity ID",
"unit_of_measurement": "Unit of measurement",
"precipitation_unit": "Precipitation unit",
"precision": "Display precision",
@@ -4280,7 +4282,7 @@
"cost_stat_input": "[%key:ui::panel::config::energy::grid::flow_dialog::from::cost_stat_input%]",
"cost_entity": "[%key:ui::panel::config::energy::grid::flow_dialog::from::cost_entity%]",
"cost_entity_input": "[%key:ui::panel::config::energy::grid::flow_dialog::from::cost_entity_input%]",
"cost_entity_helper": "Any entity with a unit of `{currency}/(valid {class} unit)` (e.g. `{currency}/{unit1}` or `{currency}/{unit2}`) may be used and will be automatically converted.",
"cost_entity_helper": "Any entity with a unit of `{currency}/(valid {class} unit)` (like `{currency}/{unit1}` or `{currency}/{unit2}`) may be used and will be automatically converted.",
"cost_entity_helper_energy": "energy",
"cost_entity_helper_volume": "volume",
"cost_number": "[%key:ui::panel::config::energy::grid::flow_dialog::from::cost_number%]",
@@ -4309,7 +4311,7 @@
"cost_stat_input": "[%key:ui::panel::config::energy::grid::flow_dialog::from::cost_stat_input%]",
"cost_entity": "[%key:ui::panel::config::energy::grid::flow_dialog::from::cost_entity%]",
"cost_entity_input": "[%key:ui::panel::config::energy::grid::flow_dialog::from::cost_entity_input%]",
"cost_entity_helper": "Any entity with a unit of `{currency}/(valid water unit)` (e.g. `{currency}/gal` or `{currency}/m³`) may be used and will be automatically converted.",
"cost_entity_helper": "Any entity with a unit of `{currency}/(valid water unit)` (like `{currency}/gal` or `{currency}/m³`) may be used and will be automatically converted.",
"cost_number": "[%key:ui::panel::config::energy::grid::flow_dialog::from::cost_number%]",
"cost_number_input": "[%key:ui::panel::config::energy::grid::flow_dialog::from::cost_number%]",
"water_usage": "Water consumption",
@@ -4439,7 +4441,7 @@
},
"url": {
"caption": "Home Assistant URL",
"description": "Configure what website addresses Home Assistant should share with other devices when they need to fetch data from Home Assistant (e.g. to play text-to-speech or other hosted media).",
"description": "Configure what website addresses Home Assistant should share with other devices when they need to fetch data from Home Assistant (for example, to play text-to-speech or other hosted media).",
"internal_url_label": "Local network",
"external_url_label": "Internet",
"external_use_ha_cloud": "Use Home Assistant Cloud",
@@ -8733,7 +8735,6 @@
"no_data_period": "There is no data for this period.",
"energy_usage_graph": {
"total_consumed": "Total consumed {num} kWh",
"total_returned": "Total exported {num} kWh",
"total_usage": "+{num} kWh",
"combined_from_grid": "Combined from grid",
"consumed_solar": "Consumed solar",
@@ -9196,7 +9197,7 @@
"edit_yaml": "[%key:ui::panel::lovelace::editor::edit_view::edit_yaml%]",
"settings": {
"column_span": "Width",
"column_span_helper": "Larger sections will be made smaller to fit the display. (e.g. on mobile devices)",
"column_span_helper": "Larger sections will be made smaller to fit the display. (for example, on mobile devices)",
"background": "Background options",
"background_enabled": "Background",
"background_enabled_helper": "Display a colored background behind the section",
@@ -10009,7 +10010,7 @@
"name": "Tile",
"description": "This card gives you a quick overview of an entity. It allows you to toggle the entity, show the More info dialog or trigger custom actions.",
"color": "Color",
"color_helper": "Inactive state (e.g. off, closed) will not be colored.",
"color_helper": "Inactive state (for example, off or closed) will not be colored.",
"icon_tap_action": "Icon tap behavior",
"icon_hold_action": "Icon hold behavior",
"icon_double_tap_action": "Icon double tap behavior",
+37
View File
@@ -0,0 +1,37 @@
import { expect, test } from "vitest";
import { isTimestamp } from "../../../src/common/string/is_timestamp";
test("isTimestamp accepts valid timestamps", () => {
expect(isTimestamp("2021-06-15T08:30:00Z")).toBe(true);
expect(isTimestamp("2021-06-15 08:30:00")).toBe(true);
expect(isTimestamp("2021-06-15T08:30")).toBe(true);
expect(isTimestamp("2021-12-31T23:59:59")).toBe(true);
expect(isTimestamp("2021-06-15T08:30:00.123+02:00")).toBe(true);
expect(isTimestamp("2021-06-15T24:00")).toBe(true);
});
test("isTimestamp rejects non-timestamps", () => {
expect(isTimestamp("not a date")).toBe(false);
expect(isTimestamp("2021/06/15T08:30")).toBe(false);
expect(isTimestamp("2021-13-01T00:00")).toBe(false);
expect(isTimestamp("2021-00-01T00:00")).toBe(false);
expect(isTimestamp("2021-06-32T00:00")).toBe(false);
expect(isTimestamp("2021-06-15T25:00")).toBe(false);
});
test("isTimestamp does not allow a leading plus or minus", () => {
expect(isTimestamp("+2021-06-15T08:30")).toBe(false);
expect(isTimestamp("-2021-06-15T08:30")).toBe(false);
});
test("isTimestamp requires a time component after the date", () => {
expect(isTimestamp("2021-06-15")).toBe(false);
});
test("isTimestamp rejects week-number dates", () => {
expect(isTimestamp("2021-W24-2T08:30")).toBe(false);
});
test("isTimestamp rejects a year on its own", () => {
expect(isTimestamp("2021")).toBe(false);
});