mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-14 20:32:09 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d545d744b | |||
| f39dab2de5 | |||
| 1527117015 | |||
| 26794560ac | |||
| 976f9de8ad | |||
| 6810bc5412 | |||
| a4ca54b80b | |||
| 07f0ef0ded | |||
| cf89bb32ab | |||
| ec5cbd16d8 | |||
| 926abd7fc5 | |||
| e227bbe9a2 | |||
| f82b0b61e5 |
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}${
|
||||
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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(
|
||||
(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user