diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f48e0570b4..cd40418a5b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -11,7 +11,7 @@ body: **Please do not report issues for custom cards.** - [fr]: https://github.com/home-assistant/frontend/discussions + [fr]: https://github.com/orgs/home-assistant/discussions [releases]: https://github.com/home-assistant/home-assistant/releases - type: checkboxes attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 47563c2286..ee1e0f3ee5 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,7 @@ blank_issues_enabled: false contact_links: - name: Request a feature for the UI / Dashboards - url: https://github.com/home-assistant/frontend/discussions/category_choices + url: https://github.com/orgs/home-assistant/discussions about: Request a new feature for the Home Assistant frontend. - name: Report a bug that is NOT related to the UI / Dashboards url: https://github.com/home-assistant/core/issues diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml new file mode 100644 index 0000000000..1ee2e5f644 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -0,0 +1,53 @@ +name: Task +description: For staff only - Create a task +type: Task +body: + - type: markdown + attributes: + value: | + ## ⚠️ RESTRICTED ACCESS + + **This form is restricted to Open Home Foundation staff and authorized contributors only.** + + If you are a community member wanting to contribute, please: + - For bug reports: Use the [bug report form](https://github.com/home-assistant/frontend/issues/new?template=bug_report.yml) + - For feature requests: Submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions) + + --- + + ### For authorized contributors + + Use this form to create tasks for development work, improvements, or other actionable items that need to be tracked. + - type: textarea + id: description + attributes: + label: Description + description: | + Provide a clear and detailed description of the task that needs to be accomplished. + + Be specific about what needs to be done, why it's important, and any constraints or requirements. + placeholder: | + Describe the task, including: + - What needs to be done + - Why this task is needed + - Expected outcome + - Any constraints or requirements + validations: + required: true + - type: textarea + id: additional_context + attributes: + label: Additional context + description: | + Any additional information, links, research, or context that would be helpful. + + Include links to related issues, research, prototypes, roadmap opportunities etc. + placeholder: | + - Roadmap opportunity: [link] + - Epic: [link] + - Feature request: [link] + - Technical design documents: [link] + - Prototype/mockup: [link] + - Dependencies: [links] + validations: + required: false diff --git a/.github/workflows/restrict-task-creation.yml b/.github/workflows/restrict-task-creation.yml new file mode 100644 index 0000000000..c67230644e --- /dev/null +++ b/.github/workflows/restrict-task-creation.yml @@ -0,0 +1,58 @@ +name: Restrict task creation + +# yamllint disable-line rule:truthy +on: + issues: + types: [opened] + +jobs: + check-authorization: + runs-on: ubuntu-latest + # Only run if this is a Task issue type (from the issue form) + if: github.event.issue.issue_type == 'Task' + steps: + - name: Check if user is authorized + uses: actions/github-script@v7 + with: + script: | + const issueAuthor = context.payload.issue.user.login; + + // Check if user is an organization member + try { + await github.rest.orgs.checkMembershipForUser({ + org: 'home-assistant', + username: issueAuthor + }); + console.log(`✅ ${issueAuthor} is an organization member`); + return; // Authorized + } catch (error) { + console.log(`❌ ${issueAuthor} is not authorized to create Task issues`); + } + + // Close the issue with a comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `Hi @${issueAuthor}, thank you for your contribution!\n\n` + + `Task issues are restricted to Open Home Foundation staff and authorized contributors.\n\n` + + `If you would like to:\n` + + `- Report a bug: Please use the [bug report form](https://github.com/home-assistant/frontend/issues/new?template=bug_report.yml)\n` + + `- Request a feature: Please submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)\n\n` + + `If you believe you should have access to create Task issues, please contact the maintainers.` + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + state: 'closed' + }); + + // Add a label to indicate this was auto-closed + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['auto-closed'] + }); diff --git a/package.json b/package.json index 977211bf15..1783790e7a 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "barcode-detector": "3.0.5", "color-name": "2.0.0", "comlink": "4.4.2", - "core-js": "3.43.0", + "core-js": "3.44.0", "cropperjs": "1.6.2", "date-fns": "4.1.0", "date-fns-tz": "3.2.0", @@ -136,7 +136,7 @@ "superstruct": "2.0.2", "tinykeys": "3.0.0", "ua-parser-js": "2.0.4", - "vis-data": "7.1.9", + "vis-data": "7.1.10", "vue": "2.7.16", "vue2-daterange-picker": "0.6.8", "weekstart": "2.0.0", @@ -158,7 +158,7 @@ "@octokit/auth-oauth-device": "8.0.1", "@octokit/plugin-retry": "8.0.1", "@octokit/rest": "22.0.0", - "@rsdoctor/rspack-plugin": "1.1.7", + "@rsdoctor/rspack-plugin": "1.1.8", "@rspack/cli": "1.4.4", "@rspack/core": "1.4.4", "@types/babel__plugin-transform-runtime": "7.9.5", diff --git a/src/components/ha-floor-picker.ts b/src/components/ha-floor-picker.ts index 84617c46e4..9eef8a2274 100644 --- a/src/components/ha-floor-picker.ts +++ b/src/components/ha-floor-picker.ts @@ -433,6 +433,7 @@ export class HaFloorPicker extends LitElement { } }, }); + return; } this._setValue(value); diff --git a/src/components/map/ha-map.ts b/src/components/map/ha-map.ts index d8a2f1c102..d334ad9297 100644 --- a/src/components/map/ha-map.ts +++ b/src/components/map/ha-map.ts @@ -36,6 +36,8 @@ declare global { } } +const PROGRAMMITIC_FIT_DELAY = 250; + const getEntityId = (entity: string | HaMapEntity): string => typeof entity === "string" ? entity : entity.entity_id; @@ -113,14 +115,33 @@ export class HaMap extends ReactiveElement { private _clickCount = 0; + private _isProgrammaticFit = false; + + private _pauseAutoFit = false; + public connectedCallback(): void { + this._pauseAutoFit = false; + document.addEventListener("visibilitychange", this._handleVisibilityChange); + this._handleVisibilityChange(); super.connectedCallback(); this._loadMap(); this._attachObserver(); } + private _handleVisibilityChange = async () => { + if (!document.hidden) { + setTimeout(() => { + this._pauseAutoFit = false; + }, 500); + } + }; + public disconnectedCallback(): void { super.disconnectedCallback(); + document.removeEventListener( + "visibilitychange", + this._handleVisibilityChange + ); if (this.leafletMap) { this.leafletMap.remove(); this.leafletMap = undefined; @@ -145,7 +166,7 @@ export class HaMap extends ReactiveElement { if (changedProps.has("_loaded") || changedProps.has("entities")) { this._drawEntities(); - autoFitRequired = true; + autoFitRequired = !this._pauseAutoFit; } else if (this._loaded && oldHass && this.entities) { // Check if any state has changed for (const entity of this.entities) { @@ -154,7 +175,7 @@ export class HaMap extends ReactiveElement { this.hass!.states[getEntityId(entity)] ) { this._drawEntities(); - autoFitRequired = true; + autoFitRequired = !this._pauseAutoFit; break; } } @@ -178,7 +199,11 @@ export class HaMap extends ReactiveElement { } if (changedProps.has("zoom")) { + this._isProgrammaticFit = true; this.leafletMap!.setZoom(this.zoom); + setTimeout(() => { + this._isProgrammaticFit = false; + }, PROGRAMMITIC_FIT_DELAY); } if ( @@ -234,13 +259,30 @@ export class HaMap extends ReactiveElement { } this._clickCount++; }); + this.leafletMap.on("zoomstart", () => { + if (!this._isProgrammaticFit) { + this._pauseAutoFit = true; + } + }); + this.leafletMap.on("movestart", () => { + if (!this._isProgrammaticFit) { + this._pauseAutoFit = true; + } + }); this._loaded = true; } finally { this._loading = false; } } - public fitMap(options?: { zoom?: number; pad?: number }): void { + public fitMap(options?: { + zoom?: number; + pad?: number; + unpause_autofit?: boolean; + }): void { + if (options?.unpause_autofit) { + this._pauseAutoFit = false; + } if (!this.leafletMap || !this.Leaflet || !this.hass) { return; } @@ -250,6 +292,7 @@ export class HaMap extends ReactiveElement { !this._mapFocusZones.length && !this.layers?.length ) { + this._isProgrammaticFit = true; this.leafletMap.setView( new this.Leaflet.LatLng( this.hass.config.latitude, @@ -257,6 +300,9 @@ export class HaMap extends ReactiveElement { ), options?.zoom || this.zoom ); + setTimeout(() => { + this._isProgrammaticFit = false; + }, PROGRAMMITIC_FIT_DELAY); return; } @@ -277,8 +323,11 @@ export class HaMap extends ReactiveElement { }); bounds = bounds.pad(options?.pad ?? 0.5); - + this._isProgrammaticFit = true; this.leafletMap.fitBounds(bounds, { maxZoom: options?.zoom || this.zoom }); + setTimeout(() => { + this._isProgrammaticFit = false; + }, PROGRAMMITIC_FIT_DELAY); } public fitBounds( @@ -291,7 +340,11 @@ export class HaMap extends ReactiveElement { const bounds = this.Leaflet.latLngBounds(boundingbox).pad( options?.pad ?? 0.5 ); + this._isProgrammaticFit = true; this.leafletMap.fitBounds(bounds, { maxZoom: options?.zoom || this.zoom }); + setTimeout(() => { + this._isProgrammaticFit = false; + }, PROGRAMMITIC_FIT_DELAY); } private _drawLayers(prevLayers: Layer[] | undefined): void { diff --git a/src/data/energy.ts b/src/data/energy.ts index 3510c92125..073ed41c7f 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -1109,21 +1109,31 @@ export const computeConsumptionSingle = (data: { export const formatConsumptionShort = ( hass: HomeAssistant, consumption: number | null, - unit: string + unit: string, + targetUnit?: string ): string => { - if (!consumption) { - return `0 ${unit}`; - } const units = ["Wh", "kWh", "MWh", "GWh", "TWh"]; let pickedUnit = unit; - let val = consumption; + let val = consumption || 0; + let targetUnitIndex = -1; + if (targetUnit) { + targetUnitIndex = units.findIndex((u) => u === targetUnit); + } let unitIndex = units.findIndex((u) => u === unit); if (unitIndex >= 0) { - while (Math.abs(val) < 1 && unitIndex > 0) { + while ( + targetUnitIndex > -1 + ? targetUnitIndex < unitIndex + : Math.abs(val) < 1 && unitIndex > 0 + ) { val *= 1000; unitIndex--; } - while (Math.abs(val) >= 1000 && unitIndex < units.length - 1) { + while ( + targetUnitIndex > -1 + ? targetUnitIndex > unitIndex + : Math.abs(val) >= 1000 && unitIndex < units.length - 1 + ) { val /= 1000; unitIndex++; } diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 2a03334b9b..34af84e248 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -438,7 +438,10 @@ class DataEntryFlowDialog extends LitElement { return; } - this._loading = "loading_step"; + const delayedLoading = setTimeout(() => { + // only show loading for slow steps to avoid flickering + this._loading = "loading_step"; + }, 250); let _step: DataEntryFlowStep; try { _step = await step; @@ -452,6 +455,7 @@ class DataEntryFlowDialog extends LitElement { }); return; } finally { + clearTimeout(delayedLoading); this._loading = undefined; } diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index ad69a73087..fb60450bb5 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -19,6 +19,7 @@ import { formatShortDateTime } from "../../../common/datetime/format_date_time"; import { storage } from "../../../common/decorators/storage"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name"; +import { computeFloorName } from "../../../common/entity/compute_floor_name"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { PROTOCOL_INTEGRATIONS, @@ -424,6 +425,18 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { (lbl) => labelReg!.find((label) => label.label_id === lbl)! ); + let floorName = "—"; + if ( + device.area_id && + areas[device.area_id]?.floor_id && + this.hass.floors + ) { + const floorId = areas[device.area_id].floor_id; + if (this.hass.floors[floorId!]) { + floorName = computeFloorName(this.hass.floors[floorId!]); + } + } + return { ...device, name: computeDeviceNameDisplay( @@ -441,6 +454,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { device.area_id && areas[device.area_id] ? areas[device.area_id].name : "—", + floor: floorName, integration: deviceEntries.length ? deviceEntries .map( @@ -524,6 +538,14 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { groupable: true, minWidth: "120px", }, + floor: { + title: localize("ui.panel.config.devices.data_table.floor"), + sortable: true, + filterable: true, + groupable: true, + minWidth: "120px", + defaultHidden: true, + }, integration: { title: localize("ui.panel.config.devices.data_table.integration"), sortable: true, diff --git a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts index 60a0a801db..9d5491572e 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts @@ -255,6 +255,20 @@ class HuiEnergyDistrubutionCard (batteryFromGrid || 0) + (batteryToGrid || 0); + // Coerce all energy numbers to the same unit (the biggest) + const maxEnergy = Math.max( + lowCarbonEnergy || 0, + totalSolarProduction || 0, + returnedToGrid || 0, + totalFromGrid || 0, + totalHomeConsumption, + totalBatteryIn || 0, + totalBatteryOut || 0 + ); + const targetEnergyUnit = formatConsumptionShort(this.hass, maxEnergy, "kWh") + .split(" ") + .pop(); + return html`
@@ -281,7 +295,8 @@ class HuiEnergyDistrubutionCard ${formatConsumptionShort( this.hass, lowCarbonEnergy, - "kWh" + "kWh", + targetEnergyUnit )} @@ -300,7 +315,8 @@ class HuiEnergyDistrubutionCard ${formatConsumptionShort( this.hass, totalSolarProduction, - "kWh" + "kWh", + targetEnergyUnit )}
` @@ -396,7 +412,8 @@ class HuiEnergyDistrubutionCard >${formatConsumptionShort( this.hass, returnedToGrid, - "kWh" + "kWh", + targetEnergyUnit )} ` : ""} @@ -409,7 +426,8 @@ class HuiEnergyDistrubutionCard : ""}${formatConsumptionShort( this.hass, totalFromGrid, - "kWh" + "kWh", + targetEnergyUnit )} @@ -432,7 +450,8 @@ class HuiEnergyDistrubutionCard ${formatConsumptionShort( this.hass, totalHomeConsumption, - "kWh" + "kWh", + targetEnergyUnit )} ${homeSolarCircumference !== undefined || homeLowCarbonCircumference !== undefined @@ -535,7 +554,8 @@ class HuiEnergyDistrubutionCard >${formatConsumptionShort( this.hass, totalBatteryIn, - "kWh" + "kWh", + targetEnergyUnit )} @@ -546,7 +566,8 @@ class HuiEnergyDistrubutionCard >${formatConsumptionShort( this.hass, totalBatteryOut, - "kWh" + "kWh", + targetEnergyUnit )} diff --git a/src/panels/lovelace/cards/hui-map-card.ts b/src/panels/lovelace/cards/hui-map-card.ts index 7c0de67ea7..74160b8be6 100644 --- a/src/panels/lovelace/cards/hui-map-card.ts +++ b/src/panels/lovelace/cards/hui-map-card.ts @@ -239,7 +239,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { )} .path=${mdiImageFilterCenterFocus} style=${isDarkMode ? "color:#ffffff" : "color:#000000"} - @click=${this._fitMap} + @click=${this._resetFocus} tabindex="0" > @@ -389,8 +389,8 @@ class HuiMapCard extends LitElement implements LovelaceCard { : (root.style.paddingBottom = "100%"); } - private _fitMap() { - this._map?.fitMap(); + private _resetFocus() { + this._map?.fitMap({ unpause_autofit: true }); } private _toggleClusterMarkers() { diff --git a/src/panels/lovelace/cards/hui-weather-forecast-card.ts b/src/panels/lovelace/cards/hui-weather-forecast-card.ts index aa5d4ff488..ccb852619c 100644 --- a/src/panels/lovelace/cards/hui-weather-forecast-card.ts +++ b/src/panels/lovelace/cards/hui-weather-forecast-card.ts @@ -464,10 +464,11 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { if (this._config?.show_forecast !== false) { rows += 1; min_rows += 1; + if (this._config?.forecast_type === "daily") { + rows += 1; + } } - if (this._config?.forecast_type === "daily") { - rows += 1; - } + return { columns: 12, rows: rows, diff --git a/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts index ccc286cca5..dc77b57c2a 100644 --- a/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts @@ -57,7 +57,7 @@ export class HuiWeatherForecastCardEditor if ( /* cannot show forecast in case it is unavailable on the entity */ - (config.show_forecast === true && this._hasForecast === false) || + (config.show_forecast !== false && this._hasForecast === false) || /* cannot hide both weather and forecast, need one of them */ (config.show_current === false && config.show_forecast === false) ) { @@ -65,6 +65,7 @@ export class HuiWeatherForecastCardEditor fireEvent(this, "config-changed", { config: { ...config, show_current: true, show_forecast: false }, }); + return; } if ( !config.forecast_type || diff --git a/src/translations/en.json b/src/translations/en.json index 6131205fcc..bc326a2152 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5093,6 +5093,7 @@ "manufacturer": "Manufacturer", "model": "Model", "area": "Area", + "floor": "Floor", "integration": "Integration", "battery": "Battery", "disabled_by": "Disabled", @@ -5626,7 +5627,7 @@ "other_networks": "Other networks", "my_network": "Preferred network", "no_preferred_network": "You don't have a preferred network yet.", - "more_info": "More Info", + "more_info": "More information", "add_open_thread_border_router": "Add an OpenThread border router", "reset_border_router": "Reset border router", "add_to_my_network": "Add to preferred network", @@ -8465,7 +8466,7 @@ "filter_states": "Filter states", "filter_attributes": "Filter attributes", "no_entities": "No entities", - "more_info": "More Info", + "more_info": "More info", "alert_entity_field": "Entity is a mandatory field", "last_updated": "[%key:ui::dialogs::more_info_control::last_updated%]", "last_changed": "[%key:ui::dialogs::more_info_control::last_changed%]", @@ -8625,7 +8626,7 @@ "input_select": "Input selects", "template": "Template entities", "universal": "Universal media player entities", - "rest": "Rest entities and notify services", + "rest": "REST entities and notify services", "command_line": "Command line entities", "filter": "Filter entities", "statistics": "Statistics entities", @@ -9061,7 +9062,7 @@ }, "host_pid": { "title": "Host processes namespace", - "description": "Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Home Assistant system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on." + "description": "Usually, the processes run by the add-on are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Home Assistant system, which adds security risks and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on." }, "apparmor": { "title": "AppArmor", diff --git a/test/common/url/search-params.test.ts b/test/common/url/search-params.test.ts new file mode 100644 index 0000000000..f77d88bb59 --- /dev/null +++ b/test/common/url/search-params.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it, vi, afterEach } from "vitest"; + +import { + addSearchParam, + createSearchParam, + extractSearchParam, + extractSearchParamsObject, + removeSearchParam, +} from "../../../src/common/url/search-params"; + +const sortQueryString = (querystring: string): string => + querystring.split("&").sort().join("&"); + +vi.mock("../../../src/common/dom/get_main_window", () => ({ + mainWindow: { location: { search: "?param1=ab+c¶m2" } }, +})); + +afterEach(() => { + vi.resetAllMocks(); +}); + +describe("Search Params Tests", () => { + it("should extract all search params from window object", () => { + expect(extractSearchParamsObject()).toEqual({ param1: "ab c", param2: "" }); + }); + + it("should return value for specified search param from window object", () => { + expect(extractSearchParam("param1")).toEqual("ab c"); + }); + + it("should create query string from given object", () => { + expect( + sortQueryString(createSearchParam({ param1: "ab c", param2: "" })) + ).toEqual(sortQueryString("param1=ab+c¶m2=")); + }); + + it("should return query string which combines provided param object and window.location.search", () => { + expect( + sortQueryString(addSearchParam({ param4: "", param3: "x y" })) + ).toEqual(sortQueryString("param1=ab+c¶m2=¶m3=x+y¶m4=")); + }); + + it("should return query string from window.location.search but remove the provided param from it", () => { + expect(sortQueryString(removeSearchParam("param2"))).toEqual( + sortQueryString("param1=ab+c") + ); + }); +}); diff --git a/test/data/energy.test.ts b/test/data/energy.test.ts index 02d8fe170e..f7fe5605a3 100644 --- a/test/data/energy.test.ts +++ b/test/data/energy.test.ts @@ -70,8 +70,10 @@ describe("Energy Short Format Test", () => { const hass = { locale: defaultLocale } as HomeAssistant; it("No Unit conversion", () => { assert.strictEqual(formatConsumptionShort(hass, 0, "Wh"), "0 Wh"); - assert.strictEqual(formatConsumptionShort(hass, 0, "kWh"), "0 kWh"); - assert.strictEqual(formatConsumptionShort(hass, 0, "GWh"), "0 GWh"); + assert.strictEqual(formatConsumptionShort(hass, 0, "kWh"), "0 Wh"); + assert.strictEqual(formatConsumptionShort(hass, 0, "kWh", "kWh"), "0 kWh"); + assert.strictEqual(formatConsumptionShort(hass, 0, "GWh"), "0 Wh"); + assert.strictEqual(formatConsumptionShort(hass, 0, "GWh", "GWh"), "0 GWh"); assert.strictEqual(formatConsumptionShort(hass, 0, "gal"), "0 gal"); assert.strictEqual( @@ -139,6 +141,36 @@ describe("Energy Short Format Test", () => { "-1.23 Wh" ); }); + it("Conversion with target unit", () => { + assert.strictEqual( + formatConsumptionShort(hass, 0.00012, "kWh", "Wh"), + "0.12 Wh" + ); + assert.strictEqual( + formatConsumptionShort(hass, 0.00012, "kWh", "kWh"), + "0 kWh" + ); + assert.strictEqual( + formatConsumptionShort(hass, 0.01012, "kWh", "kWh"), + "0.01 kWh" + ); + assert.strictEqual( + formatConsumptionShort(hass, 0.00012, "kWh", "MWh"), + "0 MWh" + ); + assert.strictEqual( + formatConsumptionShort(hass, 10.12345, "kWh", "kWh"), + "10.1 kWh" + ); + assert.strictEqual( + formatConsumptionShort(hass, 10.12345, "kWh", "ZZZZZWh"), + "10.1 kWh" + ); + assert.strictEqual( + formatConsumptionShort(hass, 151234.5678, "kWh", "MWh"), + "151 MWh" + ); + }); }); describe("Energy Usage Calculation Tests", () => { diff --git a/yarn.lock b/yarn.lock index e686b2b695..afdac67ade 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3818,22 +3818,22 @@ __metadata: languageName: node linkType: hard -"@rsdoctor/client@npm:1.1.7": - version: 1.1.7 - resolution: "@rsdoctor/client@npm:1.1.7" - checksum: 10/4d17a357414f50b8ecb52e22530b6e657106c70b9c94d41f90f2ed2c833fddede80b3d23fb1aba47584719fa704391413124010d3a48ff065a8666dd07028ec8 +"@rsdoctor/client@npm:1.1.8": + version: 1.1.8 + resolution: "@rsdoctor/client@npm:1.1.8" + checksum: 10/fe815e1d6f96a75dcc44d3da25ba1dbf30e07a448809f2c17c91b8d107278b62488f302735cde148c9d64ab4dd9920f0d2fa62c7511a82a328c29db20b903fbd languageName: node linkType: hard -"@rsdoctor/core@npm:1.1.7": - version: 1.1.7 - resolution: "@rsdoctor/core@npm:1.1.7" +"@rsdoctor/core@npm:1.1.8": + version: 1.1.8 + resolution: "@rsdoctor/core@npm:1.1.8" dependencies: "@rsbuild/plugin-check-syntax": "npm:1.3.0" - "@rsdoctor/graph": "npm:1.1.7" - "@rsdoctor/sdk": "npm:1.1.7" - "@rsdoctor/types": "npm:1.1.7" - "@rsdoctor/utils": "npm:1.1.7" + "@rsdoctor/graph": "npm:1.1.8" + "@rsdoctor/sdk": "npm:1.1.8" + "@rsdoctor/types": "npm:1.1.8" + "@rsdoctor/utils": "npm:1.1.8" axios: "npm:^1.10.0" browserslist-load-config: "npm:^1.0.0" enhanced-resolve: "npm:5.12.0" @@ -3844,50 +3844,50 @@ __metadata: semver: "npm:^7.7.2" source-map: "npm:^0.7.4" webpack-bundle-analyzer: "npm:^4.10.2" - checksum: 10/30a0adf465501cdaab1b8422529d21224935f61fb773a52075be4ba9d8ca684140bc1220e2ef1f77fbd75944645a564bd7eaba930dedb8c49f267fe0dcd99a73 + checksum: 10/1c71d9e2c25d8d7f52095c19be3c8784acb9ad9702103a5718d1c25bb20378af0490c3e95566f57c9aaf8e8c0c4b462dce75a5c1bbefb47021aff7365ae95b9c languageName: node linkType: hard -"@rsdoctor/graph@npm:1.1.7": - version: 1.1.7 - resolution: "@rsdoctor/graph@npm:1.1.7" +"@rsdoctor/graph@npm:1.1.8": + version: 1.1.8 + resolution: "@rsdoctor/graph@npm:1.1.8" dependencies: - "@rsdoctor/types": "npm:1.1.7" - "@rsdoctor/utils": "npm:1.1.7" + "@rsdoctor/types": "npm:1.1.8" + "@rsdoctor/utils": "npm:1.1.8" lodash.unionby: "npm:^4.8.0" socket.io: "npm:4.8.1" source-map: "npm:^0.7.4" - checksum: 10/4314beb5119c7082df8b046c23fa27e4bcd80e3a504188e8a9e0d84e58713fae32e720b2cc4639f04ef53e891a323871e9378d37548769961a87934eff79825d + checksum: 10/26553f153ec865b20aa1e112a57147c2282580579885595fda1b5269c00389fdead5f31ba4afdda4c5a229d7ae5ab3b70a5859bbcaddaeb835a232cb3d3dae8f languageName: node linkType: hard -"@rsdoctor/rspack-plugin@npm:1.1.7": - version: 1.1.7 - resolution: "@rsdoctor/rspack-plugin@npm:1.1.7" +"@rsdoctor/rspack-plugin@npm:1.1.8": + version: 1.1.8 + resolution: "@rsdoctor/rspack-plugin@npm:1.1.8" dependencies: - "@rsdoctor/core": "npm:1.1.7" - "@rsdoctor/graph": "npm:1.1.7" - "@rsdoctor/sdk": "npm:1.1.7" - "@rsdoctor/types": "npm:1.1.7" - "@rsdoctor/utils": "npm:1.1.7" + "@rsdoctor/core": "npm:1.1.8" + "@rsdoctor/graph": "npm:1.1.8" + "@rsdoctor/sdk": "npm:1.1.8" + "@rsdoctor/types": "npm:1.1.8" + "@rsdoctor/utils": "npm:1.1.8" lodash: "npm:^4.17.21" peerDependencies: "@rspack/core": "*" peerDependenciesMeta: "@rspack/core": optional: true - checksum: 10/c2a4dfcf5bd18b59e1acadac62f8650847a634dfe469a5b15c217d94856f6a66170b743cc16af027f0c8096f05914423dac72fd2cfd453d95e1750918506d2b7 + checksum: 10/d01a41f19e812ba6eb90af57e8e376e70936fdac15c945d4e9787521a4acea03d808202a9afab6725df0c68a0521be8e7ffce50567a2be4d7a4d4c1bf1eecde0 languageName: node linkType: hard -"@rsdoctor/sdk@npm:1.1.7": - version: 1.1.7 - resolution: "@rsdoctor/sdk@npm:1.1.7" +"@rsdoctor/sdk@npm:1.1.8": + version: 1.1.8 + resolution: "@rsdoctor/sdk@npm:1.1.8" dependencies: - "@rsdoctor/client": "npm:1.1.7" - "@rsdoctor/graph": "npm:1.1.7" - "@rsdoctor/types": "npm:1.1.7" - "@rsdoctor/utils": "npm:1.1.7" + "@rsdoctor/client": "npm:1.1.8" + "@rsdoctor/graph": "npm:1.1.8" + "@rsdoctor/types": "npm:1.1.8" + "@rsdoctor/utils": "npm:1.1.8" "@types/fs-extra": "npm:^11.0.4" body-parser: "npm:1.20.3" cors: "npm:2.8.5" @@ -3895,18 +3895,18 @@ __metadata: fs-extra: "npm:^11.1.1" json-cycle: "npm:^1.5.0" lodash: "npm:^4.17.21" - open: "npm:^10.1.2" + open: "npm:^8.4.2" sirv: "npm:2.0.4" socket.io: "npm:4.8.1" source-map: "npm:^0.7.4" tapable: "npm:2.2.2" - checksum: 10/e2b24eb7ac5aaf2872a4f4d3483be65ca4e9d17311a1fc1b4648c05cb697b232a2be1be3a3f5dc2afbcfb2d92f4373fa2b5d80d926a49bb1cb7238a8e1e5c41f + checksum: 10/b2b732a6bef8116b422b35dfdbd2805b556a169177d93e86ac20274bf160de8dd9be7bc50ad6aca0adddbac147353309166c4e2ba48e7e4741ecbd71a8b823ef languageName: node linkType: hard -"@rsdoctor/types@npm:1.1.7": - version: 1.1.7 - resolution: "@rsdoctor/types@npm:1.1.7" +"@rsdoctor/types@npm:1.1.8": + version: 1.1.8 + resolution: "@rsdoctor/types@npm:1.1.8" dependencies: "@types/connect": "npm:3.4.38" "@types/estree": "npm:1.0.5" @@ -3920,16 +3920,16 @@ __metadata: optional: true webpack: optional: true - checksum: 10/fd5aec14068746fb25dda4e93c6a804b01970dfe0c5d4a7f30dc6097923dc19b105fd5e899b279c066cc027666fa814c401bbdb1a3988a31dea8686830856050 + checksum: 10/abecd025399cafeddb563789c88d259129974aa5092993db32bcee5674987a99f05a047c0a3b728fd23c7d8f0a850e303c12ba7404e13a8f3dc2fd5214495096 languageName: node linkType: hard -"@rsdoctor/utils@npm:1.1.7": - version: 1.1.7 - resolution: "@rsdoctor/utils@npm:1.1.7" +"@rsdoctor/utils@npm:1.1.8": + version: 1.1.8 + resolution: "@rsdoctor/utils@npm:1.1.8" dependencies: "@babel/code-frame": "npm:7.26.2" - "@rsdoctor/types": "npm:1.1.7" + "@rsdoctor/types": "npm:1.1.8" "@types/estree": "npm:1.0.5" acorn: "npm:^8.10.0" acorn-import-attributes: "npm:^1.9.5" @@ -3945,7 +3945,7 @@ __metadata: picocolors: "npm:^1.1.1" rslog: "npm:^1.2.8" strip-ansi: "npm:^6.0.1" - checksum: 10/70b1fbf149f79d574c889f39e01303898ced2215d6a964d0359c7286e0609f6b9a057a5b54913b76a27312ca0469995bd05d2074fbc82879c59331cef1143ee2 + checksum: 10/74fc27f2878d044da0056c02d34bd6a893471fde240130cec01c0017ba8a85799c08b51203ae4ad0cd2481ac58ac029d49472d58433e7c7bc09bc2b298a9920c languageName: node linkType: hard @@ -6961,10 +6961,10 @@ __metadata: languageName: node linkType: hard -"core-js@npm:3.43.0": - version: 3.43.0 - resolution: "core-js@npm:3.43.0" - checksum: 10/514952992863266b1a6a2d3c985e905461d37fe72d131d9320d5dbf01ac7e746f6fc53004b548347518cc832f7d2602b9a228acf6b5183e5cbede9dd296d73d3 +"core-js@npm:3.44.0": + version: 3.44.0 + resolution: "core-js@npm:3.44.0" + checksum: 10/759ef4ab0d12c9a6e8a32537d2b0fe64c2d7be40e13e0d7eb9604a970c380d64f37489dee03bd1c286c169e47a69d3ca2a968e8fcde0f78094ea22a20465d763 languageName: node linkType: hard @@ -7258,6 +7258,13 @@ __metadata: languageName: node linkType: hard +"define-lazy-prop@npm:^2.0.0": + version: 2.0.0 + resolution: "define-lazy-prop@npm:2.0.0" + checksum: 10/0115fdb065e0490918ba271d7339c42453d209d4cb619dfe635870d906731eff3e1ade8028bb461ea27ce8264ec5e22c6980612d332895977e89c1bbc80fcee2 + languageName: node + linkType: hard + "define-lazy-prop@npm:^3.0.0": version: 3.0.0 resolution: "define-lazy-prop@npm:3.0.0" @@ -9367,7 +9374,7 @@ __metadata: "@octokit/plugin-retry": "npm:8.0.1" "@octokit/rest": "npm:22.0.0" "@replit/codemirror-indentation-markers": "npm:6.5.3" - "@rsdoctor/rspack-plugin": "npm:1.1.7" + "@rsdoctor/rspack-plugin": "npm:1.1.8" "@rspack/cli": "npm:1.4.4" "@rspack/core": "npm:1.4.4" "@shoelace-style/shoelace": "npm:2.20.1" @@ -9406,7 +9413,7 @@ __metadata: browserslist-useragent-regexp: "npm:4.1.3" color-name: "npm:2.0.0" comlink: "npm:4.4.2" - core-js: "npm:3.43.0" + core-js: "npm:3.44.0" cropperjs: "npm:1.6.2" date-fns: "npm:4.1.0" date-fns-tz: "npm:3.2.0" @@ -9479,7 +9486,7 @@ __metadata: typescript: "npm:5.8.3" typescript-eslint: "npm:8.35.1" ua-parser-js: "npm:2.0.4" - vis-data: "npm:7.1.9" + vis-data: "npm:7.1.10" vite-tsconfig-paths: "npm:5.1.4" vitest: "npm:3.2.4" vue: "npm:2.7.16" @@ -9992,7 +9999,7 @@ __metadata: languageName: node linkType: hard -"is-docker@npm:^2.0.0": +"is-docker@npm:^2.0.0, is-docker@npm:^2.1.1": version: 2.2.1 resolution: "is-docker@npm:2.2.1" bin: @@ -11834,7 +11841,7 @@ __metadata: languageName: node linkType: hard -"open@npm:^10.0.3, open@npm:^10.1.2": +"open@npm:^10.0.3": version: 10.1.2 resolution: "open@npm:10.1.2" dependencies: @@ -11846,6 +11853,17 @@ __metadata: languageName: node linkType: hard +"open@npm:^8.4.2": + version: 8.4.2 + resolution: "open@npm:8.4.2" + dependencies: + define-lazy-prop: "npm:^2.0.0" + is-docker: "npm:^2.1.1" + is-wsl: "npm:^2.2.0" + checksum: 10/acd81a1d19879c818acb3af2d2e8e9d81d17b5367561e623248133deb7dd3aefaed527531df2677d3e6aaf0199f84df57b6b2262babff8bf46ea0029aac536c9 + languageName: node + linkType: hard + "opener@npm:^1.5.2": version: 1.5.2 resolution: "opener@npm:1.5.2" @@ -14895,13 +14913,13 @@ __metadata: languageName: node linkType: hard -"vis-data@npm:7.1.9": - version: 7.1.9 - resolution: "vis-data@npm:7.1.9" +"vis-data@npm:7.1.10": + version: 7.1.10 + resolution: "vis-data@npm:7.1.10" peerDependencies: - uuid: ^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + uuid: ^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 vis-util: ^5.0.1 - checksum: 10/13cc6774cc225aa8a84d12c29d90188627fe8d937a93080113bd7b30d729fe8d6b2703322b41614ed48ad3f28f66e4090dfb1dce2ae7885a8938c2cffd7eebf3 + checksum: 10/23fb2ef26864153013372e1d95107765be86dd9ce96f987bf99fdd93759fbe5ec1bd2603d354ca18a03f0fb607b829396ec02fe005aead63ef24599512f21402 languageName: node linkType: hard