Compare commits

..

10 Commits

Author SHA1 Message Date
Aidan Timson b72b6c77bf Developer Tools -> Tools: Frontend panels (#52927)
* Rename

* Rename developer tools element tags and imports

* Point config panel routing at /config/tools

* Redirect old developer tools URLs to /config/tools

* Add tools my-link redirects

* Rename developer tools panel to Tools

* Update e2e tests for the tools panel

* Update developer tools link in issue template

* Rename developer tools translation keys to tools

* Load config fragment for statistics repairs

* Update bug report description

* Redirect old developer tools URLs on initial load and add tools e2e tests

* Casing

Co-authored-by: Norbert Rittel <norbert@rittel.de>

---------

Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-07-02 19:43:52 +02:00
Petar Petrov 7f0ddae91e Allow negative number entry in number selector on iOS (#52925)
Allow entering negative numbers in number selector on iOS
2026-07-02 19:42:03 +02:00
Petar Petrov 71e4303fa5 Fix double bar in energy devices detail graph at start of day (#52939) 2026-07-02 19:39:17 +02:00
Aidan Timson 9bb7704a3a Add e2e app tests for sidebar and map (#52950)
* Add app e2e sidebar and map coverage

* Add more app e2e sidebar coverage
2026-07-02 19:36:59 +02:00
Aidan Timson 3cc9817b90 Standardise Netlify deploy URLs with json flag (#52956) 2026-07-02 17:44:09 +02:00
renovate[bot] 674755e430 Update dependency prettier to v3.9.3 (#52947)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-02 15:14:39 +03:00
Franck Nijhof f84664909f Link the config pane help icon to the dedicated docs page (#52940)
For built-in triggers, conditions, and actions, the help icon in the editor
config pane (and Developer Tools) linked to the integration page. Point it at
the dedicated page for that specific trigger/condition/action instead, e.g.
/triggers/air_quality.co2_changed. Custom integrations keep their own
documentation URL.
2026-07-02 14:26:57 +03:00
Franck Nijhof 4fd631f229 Refresh the template tool documentation panel (#52941)
The About templates panel still pointed at the upstream Jinja2 docs and a
single extensions page. Rewrite the intro and link to the current templating
documentation instead: the learning guide (introduction, working with states,
debugging) and the searchable template functions reference.
2026-07-02 14:18:07 +03:00
renovate[bot] 18cf41b793 Update dependency @rsdoctor/rspack-plugin to v1.5.17 (#52943)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-02 14:14:44 +03:00
renovate[bot] e28788cb95 Update dependency idb-keyval to v6.2.6 (#52944)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-02 14:14:24 +03:00
60 changed files with 747 additions and 4042 deletions
+1 -1
View File
@@ -67,7 +67,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
<!--
If your issue is about how an entity is shown in the UI, please add the state
and attributes for all situations with a screenshot of the UI.
You can find this information at `/config/developer-tools/state`
You can find this information at `/config/tools/state`
-->
```yaml
+2 -2
View File
@@ -94,8 +94,8 @@ body:
label: State of relevant entities
description: >
If your issue is about how an entity is shown in the UI, please add the
state and attributes for all situations. You can find this information
at Developer Tools -> States.
state and attributes for all situations. You can find this
information in the Details view of the More info dialog.
render: txt
- type: textarea
attributes:
+6 -4
View File
@@ -21,7 +21,7 @@ jobs:
if: github.event_name != 'push'
environment:
name: Cast Development
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
url: ${{ steps.deploy.outputs.netlify_url }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -46,7 +46,8 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli deploy --dir=cast/dist --alias dev
npx -y netlify-cli deploy --dir=cast/dist --alias dev --json > deploy_output.json
echo "netlify_url=$(jq -r '.url // .deploy_url' deploy_output.json)" >> "$GITHUB_OUTPUT"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
@@ -57,7 +58,7 @@ jobs:
if: github.event_name == 'push'
environment:
name: Cast Production
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
url: ${{ steps.deploy.outputs.netlify_url }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -82,7 +83,8 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli deploy --dir=cast/dist --prod
npx -y netlify-cli deploy --dir=cast/dist --prod --json > deploy_output.json
echo "netlify_url=$(jq -r '.url // .deploy_url' deploy_output.json)" >> "$GITHUB_OUTPUT"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
+6 -4
View File
@@ -22,7 +22,7 @@ jobs:
if: github.event_name != 'push' || github.ref_name != 'master'
environment:
name: Demo Development
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
url: ${{ steps.deploy.outputs.netlify_url }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -47,7 +47,8 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli deploy --dir=demo/dist --prod
npx -y netlify-cli deploy --dir=demo/dist --prod --json > deploy_output.json
echo "netlify_url=$(jq -r '.url // .deploy_url' deploy_output.json)" >> "$GITHUB_OUTPUT"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
@@ -58,7 +59,7 @@ jobs:
if: github.event_name == 'push' && github.ref_name == 'master'
environment:
name: Demo Production
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
url: ${{ steps.deploy.outputs.netlify_url }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -83,7 +84,8 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli deploy --dir=demo/dist --prod
npx -y netlify-cli deploy --dir=demo/dist --prod --json > deploy_output.json
echo "netlify_url=$(jq -r '.url // .deploy_url' deploy_output.json)" >> "$GITHUB_OUTPUT"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }}
+3 -2
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
environment:
name: Design
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
url: ${{ steps.deploy.outputs.netlify_url }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -40,7 +40,8 @@ jobs:
- name: Deploy to Netlify
id: deploy
run: |
npx -y netlify-cli deploy --dir=gallery/dist --prod
npx -y netlify-cli deploy --dir=gallery/dist --prod --json > deploy_output.json
echo "netlify_url=$(jq -r '.url // .deploy_url' deploy_output.json)" >> "$GITHUB_OUTPUT"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}
+2 -3
View File
@@ -47,11 +47,10 @@ jobs:
run: |
npx -y netlify-cli deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \
--json > deploy_output.json
echo "netlify_url=$(jq -r '.url // .deploy_url' deploy_output.json)" >> "$GITHUB_OUTPUT"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}
- name: Generate summary
run: |
NETLIFY_LIVE_URL=$(jq -r '.deploy_url' deploy_output.json)
echo "$NETLIFY_LIVE_URL" >> "$GITHUB_STEP_SUMMARY"
run: echo "${{ steps.deploy.outputs.netlify_url }}" >> "$GITHUB_STEP_SUMMARY"
+3 -3
View File
@@ -102,7 +102,7 @@
"gulp-zopfli-green": "7.0.0",
"hls.js": "1.6.16",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.5",
"idb-keyval": "6.2.6",
"intl-messageformat": "11.2.9",
"js-yaml": "5.2.0",
"leaflet": "1.9.4",
@@ -147,7 +147,7 @@
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@playwright/test": "1.61.1",
"@rsdoctor/rspack-plugin": "1.5.16",
"@rsdoctor/rspack-plugin": "1.5.17",
"@rspack/core": "2.1.1",
"@rspack/dev-server": "2.1.0",
"@types/babel__plugin-transform-runtime": "7.9.5",
@@ -198,7 +198,7 @@
"map-stream": "0.0.7",
"minify-literals": "2.0.2",
"pinst": "3.0.0",
"prettier": "3.9.1",
"prettier": "3.9.3",
"rspack-manifest-plugin": "5.2.2",
"serve": "14.2.6",
"sinon": "22.0.0",
+3 -21
View File
@@ -1,16 +1,8 @@
import type { BarSeriesOption } from "echarts/types/dist/shared";
/**
* `extraBuckets` (only used when `stacked`) seeds the bucket union with the
* expected statistics grid so sparse datasets get zero-filled across the whole
* range, including past their last real point. Without it, buckets are only
* derived from the data and trailing buckets are never filled (legacy
* behavior, kept for callers that don't pass a grid).
*/
export function fillDataGapsAndRoundCaps(
datasets: BarSeriesOption[],
stacked = true,
extraBuckets?: number[]
stacked = true
) {
if (!stacked) {
// For non-stacked charts, we can simply apply an overall border to each stack
@@ -52,7 +44,6 @@ export function fillDataGapsAndRoundCaps(
dataset.data!.map((datapoint) => Number(datapoint![0]))
)
.flat()
.concat(extraBuckets ?? [])
)
).sort((a, b) => a - b);
@@ -70,18 +61,9 @@ export function fillDataGapsAndRoundCaps(
const x = item.value?.[0];
const stack = datasets[i].stack ?? "";
if (x === undefined) {
// Past the end of this dataset's data. Only append trailing buckets
// when an explicit grid was provided; originally-empty datasets
// (e.g. compare placeholders) stay empty either way.
if (
dataPoint !== undefined ||
extraBuckets === undefined ||
!datasets[i].data!.length
) {
continue;
}
continue;
}
if (x === undefined || Number(x) !== bucket) {
if (Number(x) !== bucket) {
datasets[i].data?.splice(index, 0, {
value: [bucket, 0],
itemStyle: {
@@ -3,6 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { NumberSelector } from "../../data/selector";
import { isSafari } from "../../util/is_safari";
import "../ha-input-helper-text";
import "../ha-slider";
import "../input/ha-input";
@@ -66,6 +67,16 @@ export class HaNumberSelector extends LitElement {
}
}
// On iOS/iPadOS the numeric and decimal on-screen keypads have no minus key,
// so negatives can only be typed with the full "text" keyboard. Other
// platforms include a minus on their number keypads, so restrict this
// workaround to Safari/WebKit and only when the selector allows negatives
// (e.g. numeric_state triggers/conditions).
const useTextInputMode =
isSafari &&
this.selector.number?.min !== undefined &&
this.selector.number.min < 0;
const translationKey = this.selector.number?.translation_key;
let unit = this.selector.number?.unit_of_measurement;
if (isBox && unit && this.localizeValue && translationKey) {
@@ -100,11 +111,13 @@ export class HaNumberSelector extends LitElement {
: nothing
}
<ha-input
.inputMode=${
this.selector.number?.step === "any" ||
(this.selector.number?.step ?? 1) % 1 !== 0
? "decimal"
: "numeric"
.inputmode=${
useTextInputMode
? "text"
: this.selector.number?.step === "any" ||
(this.selector.number?.step ?? 1) % 1 !== 0
? "decimal"
: "numeric"
}
.label=${!isBox ? undefined : this.label}
.placeholder=${
+2 -2
View File
@@ -525,10 +525,10 @@ export class HaServiceControl extends LitElement {
this._manifest
? html` <a
href=${
this._manifest.is_built_in
this._manifest.is_built_in && this._value?.action
? documentationUrl(
this.hass,
`/integrations/${this._manifest.domain}`
`/actions/${this._value.action}`
)
: this._manifest.documentation
}
+2 -2
View File
@@ -57,8 +57,8 @@ export const CONFIG_SUB_ROUTES: Record<
translationKey: "ui.components.navigation-picker.route.scripts",
iconPath: mdiScriptText,
},
"developer-tools": {
translationKey: "ui.components.navigation-picker.route.developer_tools",
tools: {
translationKey: "ui.components.navigation-picker.route.tools",
iconPath: mdiHammer,
},
integrations: {
+17 -5
View File
@@ -28,6 +28,21 @@ const useHash = __DEMO__;
const curPath = () =>
useHash ? location.hash.substring(1) : location.pathname;
// Developer tools was renamed to Tools (/config/tools) in 2026.8; it had moved
// from /developer-tools to /config in 2026.2. Redirect both old locations to
// the new one. Applied on the initial route and on every navigation so
// bookmarks and external links to the old URLs resolve too, not just in-app
// navigation.
const redirectLegacyToolsPath = (path: string): string => {
if (path.startsWith("/config/developer-tools")) {
return path.replace("/config/developer-tools", "/config/tools");
}
if (path.startsWith("/developer-tools")) {
return path.replace("/developer-tools", "/config/tools");
}
return path;
};
const panelUrl = (path: string) => {
const dividerPos = path.indexOf("/", 1);
return dividerPos === -1 ? path.substring(1) : path.substring(1, dividerPos);
@@ -50,7 +65,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
constructor() {
super();
const path = curPath();
const path = redirectLegacyToolsPath(curPath());
this._route = {
prefix: "",
@@ -106,10 +121,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
// Navigation
const updateRoute = (path = curPath()) => {
// Developer tools panel was moved to config in 2026.2
if (path.startsWith("/developer-tools")) {
path = path.replace("/developer-tools", "/config/developer-tools");
}
path = redirectLegacyToolsPath(path);
if (this._route && path === this._route.path) {
return;
}
@@ -195,7 +195,7 @@ export class HaPlatformCondition extends LitElement {
this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._manifest.domain}`
`/conditions/${this.condition.condition}`
)
: this._manifest.documentation
}
@@ -190,7 +190,7 @@ export class HaPlatformTrigger extends LitElement {
this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._manifest.domain}`
`/triggers/${this.trigger.trigger}`
)
: this._manifest.documentation
}
+5 -5
View File
@@ -211,8 +211,8 @@ export const configSections: Record<string, PageNavigation[]> = {
adminOnly: true,
},
{
path: "/config/developer-tools",
translationKey: "developer_tools",
path: "/config/tools",
translationKey: "tools",
iconPath: mdiHammer,
iconColor: "#7A5AA6",
core: true,
@@ -328,10 +328,10 @@ export const configSections: Record<string, PageNavigation[]> = {
adminOnly: true,
},
],
developer_tools: [
tools: [
{
path: "/config/developer-tools",
translationKey: "ui.panel.config.dashboard.developer_tools.main",
path: "/config/tools",
translationKey: "ui.panel.config.dashboard.tools.main",
iconPath: mdiHammer,
iconColor: "#7A5AA6",
core: true,
+3 -3
View File
@@ -70,9 +70,9 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-system-navigation",
load: () => import("./core/ha-config-system-navigation"),
},
"developer-tools": {
tag: "ha-panel-developer-tools",
load: () => import("./developer-tools/ha-panel-developer-tools"),
tools: {
tag: "ha-panel-tools",
load: () => import("./tools/ha-panel-tools"),
cache: true,
},
logs: {
@@ -18,7 +18,7 @@ import {
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { fixStatisticsIssue } from "../developer-tools/statistics/fix-statistics";
import { fixStatisticsIssue } from "../tools/statistics/fix-statistics";
import { showVacuumSegmentMappingDialog } from "../entities/dialogs/show-dialog-vacuum-segment-mapping";
import { showRepairsFlowDialog } from "./show-dialog-repair-flow";
import { showRepairsIssueDialog } from "./show-repair-issue-dialog";
@@ -171,7 +171,7 @@ class HaConfigRepairs extends LitElement {
issue.translation_key &&
STATISTIC_TYPES.includes(issue.translation_key as any)
) {
this.hass.loadFragmentTranslation("developer-tools");
this.hass.loadFragmentTranslation("config");
const data = await fetchRepairsIssueData(
this.hass.connection,
issue.domain,
@@ -48,7 +48,7 @@ import { resolveMediaSource } from "../../../../data/media_source";
import { MatchMinHeightMixin } from "../../../../mixins/match-min-height-mixin";
import { withViewTransition } from "../../../../common/util/view-transition";
@customElement("developer-tools-action")
@customElement("tools-action")
class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -130,14 +130,12 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
const modeButtons: ToggleButton[] = [
{
label: this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.ui_mode"
),
label: this.hass.localize("ui.panel.config.tools.tabs.actions.ui_mode"),
value: "ui",
},
{
label: this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.yaml_mode"
"ui.panel.config.tools.tabs.actions.yaml_mode"
),
value: "yaml",
},
@@ -163,7 +161,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
<div class="header-row">
<div class="header-title">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.title"
"ui.panel.config.tools.tabs.actions.title"
)}
</div>
<ha-button-toggle-group
@@ -177,7 +175,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
</div>
<p class="secondary">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.description"
"ui.panel.config.tools.tabs.actions.description"
)}
</p>
</div>
@@ -220,14 +218,14 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
!this._uiAvailable
? html`<span class="error"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.no_template_ui_support"
"ui.panel.config.tools.tabs.actions.no_template_ui_support"
)}</span
>`
: nothing
}
<ha-progress-button raised @click=${this._callService}>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.call_service"
"ui.panel.config.tools.tabs.actions.call_service"
)}
</ha-progress-button>
</div>
@@ -238,7 +236,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
? html`<div class="content response">
<ha-card
.header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.response"
"ui.panel.config.tools.tabs.actions.response"
)}
>
<div class="card-content">
@@ -253,7 +251,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
slot="extra-actions"
@click=${this._copyTemplate}
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.copy_clipboard_template"
"ui.panel.config.tools.tabs.actions.copy_clipboard_template"
)}</ha-button
>
</ha-yaml-editor>
@@ -270,10 +268,10 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
.header=${
this._yamlMode
? this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.all_parameters"
"ui.panel.config.tools.tabs.actions.all_parameters"
)
: this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.yaml_parameters"
"ui.panel.config.tools.tabs.actions.yaml_parameters"
)
}
outlined
@@ -287,7 +285,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
target
? html`
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.accepts_target"
"ui.panel.config.tools.tabs.actions.accepts_target"
)}
`
: ""
@@ -322,17 +320,17 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
<tr>
<th>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.column_parameter"
"ui.panel.config.tools.tabs.actions.column_parameter"
)}
</th>
<th>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.column_description"
"ui.panel.config.tools.tabs.actions.column_description"
)}
</th>
<th>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.column_example"
"ui.panel.config.tools.tabs.actions.column_example"
)}
</th>
</tr>
@@ -371,7 +369,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
appearance="plain"
@click=${this._fillExampleData}
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.fill_example_data"
"ui.panel.config.tools.tabs.actions.fill_example_data"
)}</ha-button
>`
: ""
@@ -406,14 +404,14 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
const errorCategory = yamlMode ? "yaml" : "ui";
if (!serviceData?.action) {
return localize(
`ui.panel.config.developer-tools.tabs.actions.errors.${errorCategory}.no_action`
`ui.panel.config.tools.tabs.actions.errors.${errorCategory}.no_action`
);
}
const domain = computeDomain(serviceData.action);
const service = computeObjectId(serviceData.action);
if (!domain || !service) {
return localize(
`ui.panel.config.developer-tools.tabs.actions.errors.${errorCategory}.invalid_action`
`ui.panel.config.tools.tabs.actions.errors.${errorCategory}.invalid_action`
);
}
const dataIsTemplate =
@@ -427,7 +425,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
!serviceData.data?.area_id
) {
return localize(
`ui.panel.config.developer-tools.tabs.actions.errors.${errorCategory}.no_target`
`ui.panel.config.tools.tabs.actions.errors.${errorCategory}.no_target`
);
}
for (const field of fields) {
@@ -437,7 +435,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
(!serviceData.data || serviceData.data[field.key] === undefined)
) {
return localize(
`ui.panel.config.developer-tools.tabs.actions.errors.${errorCategory}.missing_required_field`,
`ui.panel.config.tools.tabs.actions.errors.${errorCategory}.missing_required_field`,
{ key: field.key }
);
}
@@ -496,7 +494,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
forwardHaptic(this, "failure");
button.actionError();
this._error = this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.errors.yaml.invalid_yaml"
"ui.panel.config.tools.tabs.actions.errors.yaml.invalid_yaml"
);
return;
}
@@ -569,7 +567,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
rel="noreferrer"
><ha-button>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.open_media"
"ui.panel.config.tools.tabs.actions.open_media"
)}
</ha-button></a
>
@@ -829,6 +827,6 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
declare global {
interface HTMLElementTagNameMap {
"developer-tools-action": HaPanelDevAction;
"tools-action": HaPanelDevAction;
}
}
@@ -25,7 +25,7 @@ interface SentenceParsingResult {
result: AssistDebugResult | null;
}
@customElement("developer-tools-assist")
@customElement("tools-assist")
class HaPanelDevAssist extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -118,14 +118,14 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
<div class="content">
<ha-card
.header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.assist.title"
"ui.panel.config.tools.tabs.assist.title"
)}
class="form"
>
<div class="card-content">
<p class="description">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.assist.description"
"ui.panel.config.tools.tabs.assist.description"
)}
</p>
${
@@ -143,7 +143,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
<ha-textarea
resize="auto"
.label=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.assist.sentences"
"ui.panel.config.tools.tabs.assist.sentences"
)}
id="sentences-input"
@input=${this._textAreaInput}
@@ -157,7 +157,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
.disabled=${!this._language || !this._validInput}
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.assist.parse_sentences"
"ui.panel.config.tools.tabs.assist.parse_sentences"
)}
</ha-button>
</div>
@@ -184,7 +184,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
.path=${mdiDownload}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.assist.download_results"
"ui.panel.config.tools.tabs.assist.download_results"
)}
</ha-button>
</div>
@@ -204,7 +204,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
</div>
<div class="info">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.assist.language"
"ui.panel.config.tools.tabs.assist.language"
)}:
${formatLanguageCode(language, this.hass.locale)}
(${language})
@@ -221,7 +221,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
`
: html`<ha-alert alert-type="error">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.assist.no_match"
"ui.panel.config.tools.tabs.assist.no_match"
)}
</ha-alert>`
}
@@ -304,6 +304,6 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
declare global {
interface HTMLElementTagNameMap {
"developer-tools-assist": HaPanelDevAssist;
"tools-assist": HaPanelDevAssist;
}
}
@@ -17,12 +17,12 @@ class HaDebugConnectionRow extends LitElement {
<ha-list-item-base>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.debug_connection.title"
"ui.panel.config.tools.tabs.debug.debug_connection.title"
)}</span
>
<span slot="supporting-text"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.debug_connection.description"
"ui.panel.config.tools.tabs.debug.debug_connection.description"
)}</span
>
<ha-switch
@@ -20,12 +20,12 @@ class HaDebugDisableViewTransitionRow extends LitElement {
<ha-list-item-base>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.disable_view_transition.title"
"ui.panel.config.tools.tabs.debug.disable_view_transition.title"
)}</span
>
<span slot="supporting-text"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.disable_view_transition.description"
"ui.panel.config.tools.tabs.debug.disable_view_transition.description"
)}</span
>
<ha-switch
@@ -230,13 +230,13 @@ export class HaDebugViewportEnvironmentCard extends LitElement {
return html`
<ha-card
.header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.viewport_environment.title"
"ui.panel.config.tools.tabs.debug.viewport_environment.title"
)}
>
<div class="card-content">
<p class="explanation">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.viewport_environment.description"
"ui.panel.config.tools.tabs.debug.viewport_environment.description"
)}
</p>
<ha-code-editor
@@ -20,7 +20,7 @@ import "./ha-debug-connection-row";
import "./ha-debug-disable-view-transition-row";
import "./ha-debug-viewport-environment-card";
@customElement("developer-tools-debug")
@customElement("tools-debug")
class HaPanelDevDebug extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -31,7 +31,7 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
<div class="content">
<ha-card
.header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.title"
"ui.panel.config.tools.tabs.debug.title"
)}
>
<ha-list-base>
@@ -45,13 +45,13 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
</ha-card>
<ha-card
.header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.entity_diagnostic.title"
"ui.panel.config.tools.tabs.debug.entity_diagnostic.title"
)}
>
<div class="card-content">
<ha-entity-picker
.helper=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.entity_diagnostic.description"
"ui.panel.config.tools.tabs.debug.entity_diagnostic.description"
)}
@value-changed=${this._entityPicked}
></ha-entity-picker>
@@ -62,7 +62,7 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
appearance="filled"
.disabled=${!this._entityId}
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.entity_diagnostic.copy_to_clipboard"
"ui.panel.config.tools.tabs.debug.entity_diagnostic.copy_to_clipboard"
)}</ha-button
>
</div>
@@ -136,6 +136,6 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
declare global {
interface HTMLElementTagNameMap {
"developer-tools-debug": HaPanelDevDebug;
"tools-debug": HaPanelDevDebug;
}
}
@@ -76,7 +76,7 @@ class EventSubscribeCard extends LitElement {
return html`
<ha-card
header=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.listen_to_events"
"ui.panel.config.tools.tabs.events.listen_to_events"
)}
>
<div class="card-content">
@@ -84,10 +84,10 @@ class EventSubscribeCard extends LitElement {
.label=${
this._subscribed
? this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.listening_to"
"ui.panel.config.tools.tabs.events.listening_to"
)
: this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.subscribe_to"
"ui.panel.config.tools.tabs.events.subscribe_to"
)
}
.disabled=${this._subscribed !== undefined}
@@ -96,11 +96,11 @@ class EventSubscribeCard extends LitElement {
></ha-input>
<ha-input
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.filter_events"
"ui.panel.config.tools.tabs.events.filter_events"
)}
.value=${this._eventFilter}
.disabled=${this._subscribed !== undefined}
.hint=${`${this.hass!.localize("ui.panel.config.developer-tools.tabs.events.filter_helper")}${this._ignoredEventsCount ? ` ${this.hass!.localize("ui.panel.config.developer-tools.tabs.events.filter_ignored", { count: this._ignoredEventsCount })}` : ""}`}
.hint=${`${this.hass!.localize("ui.panel.config.tools.tabs.events.filter_helper")}${this._ignoredEventsCount ? ` ${this.hass!.localize("ui.panel.config.tools.tabs.events.filter_ignored", { count: this._ignoredEventsCount })}` : ""}`}
@input=${this._filterChanged}
></ha-input>
${
@@ -118,10 +118,10 @@ class EventSubscribeCard extends LitElement {
${
this._subscribed
? this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.stop_listening"
"ui.panel.config.tools.tabs.events.stop_listening"
)
: this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.start_listening"
"ui.panel.config.tools.tabs.events.start_listening"
)
}
</ha-button>
@@ -131,7 +131,7 @@ class EventSubscribeCard extends LitElement {
@click=${this._clearEvents}
>
${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.clear_events"
"ui.panel.config.tools.tabs.events.clear_events"
)}
</ha-button>
</div>
@@ -144,10 +144,10 @@ class EventSubscribeCard extends LitElement {
if (!this._events.length) {
const message = this._subscribed
? this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.waiting_for_events"
"ui.panel.config.tools.tabs.events.waiting_for_events"
)
: this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.subscribe_prompt"
"ui.panel.config.tools.tabs.events.subscribe_prompt"
);
return html`
<ha-card class="events-card">
@@ -172,7 +172,7 @@ class EventSubscribeCard extends LitElement {
.path=${mdiChevronDoubleLeft}
.disabled=${index >= bufferTotal - 1}
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.oldest_event"
"ui.panel.config.tools.tabs.events.oldest_event"
)}
@click=${this._showOldest}
></ha-icon-button>
@@ -180,13 +180,13 @@ class EventSubscribeCard extends LitElement {
.path=${mdiChevronLeft}
.disabled=${index >= bufferTotal - 1}
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.older_event"
"ui.panel.config.tools.tabs.events.older_event"
)}
@click=${this._showOlder}
></ha-icon-button>
<div class="event-info">
${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.event_fired",
"ui.panel.config.tools.tabs.events.event_fired",
{
name: position,
time: formatTimeWithSeconds(
@@ -208,7 +208,7 @@ class EventSubscribeCard extends LitElement {
<ha-tooltip for="buffer-info" placement="bottom">
<span class="buffer-tooltip">
${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.buffer_disclaimer",
"ui.panel.config.tools.tabs.events.buffer_disclaimer",
{ count: MAX_BUFFERED_EVENTS }
)}
</span>
@@ -221,7 +221,7 @@ class EventSubscribeCard extends LitElement {
.path=${mdiChevronRight}
.disabled=${atNewest}
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.newer_event"
"ui.panel.config.tools.tabs.events.newer_event"
)}
@click=${this._showNewer}
></ha-icon-button>
@@ -229,7 +229,7 @@ class EventSubscribeCard extends LitElement {
.path=${mdiChevronDoubleRight}
.disabled=${atNewest}
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.newest_event"
"ui.panel.config.tools.tabs.events.newest_event"
)}
@click=${this._showNewest}
></ha-icon-button>
@@ -362,13 +362,13 @@ class EventSubscribeCard extends LitElement {
}, this._eventType);
} catch (error) {
this._error = this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.subscribe_failed",
"ui.panel.config.tools.tabs.events.subscribe_failed",
{
error:
error instanceof Error
? error.message
: this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.unknown_error"
"ui.panel.config.tools.tabs.events.unknown_error"
),
}
);
@@ -27,7 +27,7 @@ class EventsList extends LitElement {
>
<span>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.count_listeners",
"ui.panel.config.tools.tabs.events.count_listeners",
{
count: event.listener_count,
}
@@ -14,7 +14,7 @@ import { documentationUrl } from "../../../../util/documentation-url";
import "./event-subscribe-card";
import "./events-list";
@customElement("developer-tools-event")
@customElement("tools-event")
class HaPanelDevEvent extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -40,7 +40,7 @@ class HaPanelDevEvent extends LitElement {
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.description"
"ui.panel.config.tools.tabs.events.description"
)}
<a
href=${documentationUrl(
@@ -51,14 +51,14 @@ class HaPanelDevEvent extends LitElement {
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.documentation"
"ui.panel.config.tools.tabs.events.documentation"
)}
</a>
</p>
<div class="inputs">
<ha-input
.label=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.type"
"ui.panel.config.tools.tabs.events.type"
)}
autofocus
required
@@ -67,7 +67,7 @@ class HaPanelDevEvent extends LitElement {
></ha-input>
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.data"
"ui.panel.config.tools.tabs.events.data"
)}
</p>
</div>
@@ -85,7 +85,7 @@ class HaPanelDevEvent extends LitElement {
appearance="filled"
.disabled=${!this._isValid}
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.fire_event"
"ui.panel.config.tools.tabs.events.fire_event"
)}</ha-button
>
</div>
@@ -101,7 +101,7 @@ class HaPanelDevEvent extends LitElement {
<div>
<h2>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.active_listeners"
"ui.panel.config.tools.tabs.events.active_listeners"
)}
</h2>
<events-list
@@ -131,7 +131,7 @@ class HaPanelDevEvent extends LitElement {
if (!this._eventType) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.alert_event_type"
"ui.panel.config.tools.tabs.events.alert_event_type"
),
});
return;
@@ -143,7 +143,7 @@ class HaPanelDevEvent extends LitElement {
);
fireEvent(this, "hass-notification", {
message: this.hass.localize(
"ui.panel.config.developer-tools.tabs.events.notification_event_fired",
"ui.panel.config.tools.tabs.events.notification_event_fired",
{ type: this._eventType }
),
});
@@ -221,6 +221,6 @@ class HaPanelDevEvent extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"developer-tools-event": HaPanelDevEvent;
"tools-event": HaPanelDevEvent;
}
}
@@ -12,42 +12,42 @@ import "../../../components/ha-tab-group";
import "../../../components/ha-tab-group-tab";
import "../../../components/ha-top-app-bar-fixed";
import type { HomeAssistant, Route } from "../../../types";
import "./developer-tools-router";
import "./tools-router";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
const DEVELOPER_TOOLS_TABS = [
const TOOLS_TABS = [
{
panel: "yaml",
translationKey: "ui.panel.config.developer-tools.tabs.yaml.title",
translationKey: "ui.panel.config.tools.tabs.yaml.title",
},
{
panel: "state",
translationKey: "ui.panel.config.developer-tools.tabs.states.title",
translationKey: "ui.panel.config.tools.tabs.states.title",
},
{
panel: "action",
translationKey: "ui.panel.config.developer-tools.tabs.actions.title",
translationKey: "ui.panel.config.tools.tabs.actions.title",
},
{
panel: "template",
translationKey: "ui.panel.config.developer-tools.tabs.templates.title",
translationKey: "ui.panel.config.tools.tabs.templates.title",
},
{
panel: "event",
translationKey: "ui.panel.config.developer-tools.tabs.events.title",
translationKey: "ui.panel.config.tools.tabs.events.title",
},
{
panel: "statistics",
translationKey: "ui.panel.config.developer-tools.tabs.statistics.title",
translationKey: "ui.panel.config.tools.tabs.statistics.title",
},
{
panel: "assist",
translationKey: "ui.panel.config.developer-tools.tabs.assist.tab",
translationKey: "ui.panel.config.tools.tabs.assist.tab",
},
] as const;
@customElement("ha-panel-developer-tools")
class PanelDeveloperTools extends LitElement {
@customElement("ha-panel-tools")
class PanelTools extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@@ -68,9 +68,7 @@ class PanelDeveloperTools extends LitElement {
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
<div slot="title">
${this.hass.localize(
"ui.panel.config.dashboard.developer_tools.main"
)}
${this.hass.localize("ui.panel.config.dashboard.tools.main")}
</div>
<ha-dropdown slot="actionItems" @wa-select=${this._handleMenuAction}>
<ha-icon-button
@@ -79,13 +77,11 @@ class PanelDeveloperTools extends LitElement {
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item value="debug">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.title"
)}
${this.hass.localize("ui.panel.config.tools.tabs.debug.title")}
</ha-dropdown-item>
</ha-dropdown>
<ha-tab-group @wa-tab-show=${this._handlePageSelected} slot="subRow">
${DEVELOPER_TOOLS_TABS.map(
${TOOLS_TABS.map(
(tab) => html`
<ha-tab-group-tab
slot="nav"
@@ -93,7 +89,7 @@ class PanelDeveloperTools extends LitElement {
.active=${page === tab.panel}
>
<a
href="/config/developer-tools/${tab.panel}"
href="/config/tools/${tab.panel}"
@click=${this._handleTabAnchorClick}
>${this.hass.localize(tab.translationKey)}</a
>
@@ -101,11 +97,11 @@ class PanelDeveloperTools extends LitElement {
`
)}
</ha-tab-group>
<developer-tools-router
<tools-router
.route=${this.route}
.narrow=${this.narrow}
.hass=${this.hass}
></developer-tools-router>
></tools-router>
</ha-top-app-bar-fixed>
`;
}
@@ -124,7 +120,7 @@ class PanelDeveloperTools extends LitElement {
return;
}
if (newPage !== this._page) {
navigate(`/config/developer-tools/${newPage}`);
navigate(`/config/tools/${newPage}`);
} else {
scrollTo({ behavior: "smooth", top: 0 });
}
@@ -133,7 +129,7 @@ class PanelDeveloperTools extends LitElement {
private async _handleMenuAction(ev: HaDropdownSelectEvent) {
const action = ev.detail.item.value;
if (action === "debug") {
navigate(`/config/developer-tools/debug`);
navigate(`/config/tools/debug`);
}
}
@@ -146,7 +142,7 @@ class PanelDeveloperTools extends LitElement {
}
static readonly styles: CSSResultGroup = css`
developer-tools-router {
tools-router {
display: block;
height: 100%;
}
@@ -170,6 +166,6 @@ class PanelDeveloperTools extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-panel-developer-tools": PanelDeveloperTools;
"ha-panel-tools": PanelTools;
}
}
@@ -24,7 +24,7 @@ import { haStyle } from "../../../../resources/styles";
import { loadVirtualizer } from "../../../../resources/virtualizer";
import { showToast } from "../../../../util/toast";
@customElement("developer-tools-state-renderer")
@customElement("tools-state-renderer")
class HaPanelDevStateRenderer extends LitElement {
@property({ attribute: false }) public entities: HassEntity[] = [];
@@ -79,16 +79,12 @@ class HaPanelDevStateRenderer extends LitElement {
<div class="row" role="row" aria-rowindex="1">
<div class="header" role="columnheader">
<span class="padded">
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.entity"
)}
${this._i18n.localize("ui.panel.config.tools.tabs.states.entity")}
</span>
</div>
<div class="header" role="columnheader">
<span class="padded">
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.state"
)}
${this._i18n.localize("ui.panel.config.tools.tabs.states.state")}
</span>
</div>
<div class="header" role="columnheader" ?hidden=${!showDevice}>
@@ -106,7 +102,7 @@ class HaPanelDevStateRenderer extends LitElement {
<div class="header" role="columnheader">
<span class="padded">
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.attributes"
"ui.panel.config.tools.tabs.states.attributes"
)}
</span>
</div>
@@ -134,7 +130,7 @@ class HaPanelDevStateRenderer extends LitElement {
<div class="cell" role="cell" aria-colspan="5">
<span class="padded">
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.no_entities"
"ui.panel.config.tools.tabs.states.no_entities"
)}
</span>
</div>
@@ -195,10 +191,10 @@ class HaPanelDevStateRenderer extends LitElement {
@click=${this._copyEntity}
.entity=${item}
alt=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.copy_id"
"ui.panel.config.tools.tabs.states.copy_id"
)}
title=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.copy_id"
"ui.panel.config.tools.tabs.states.copy_id"
)}
.path=${mdiClipboardTextMultipleOutline}
></ha-svg-icon>
@@ -211,10 +207,10 @@ class HaPanelDevStateRenderer extends LitElement {
@click=${this._entityMoreInfo}
.entity=${item}
alt=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.more_info"
"ui.panel.config.tools.tabs.states.more_info"
)}
title=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.more_info"
"ui.panel.config.tools.tabs.states.more_info"
)}
.path=${mdiInformationOutline}
></ha-svg-icon>
@@ -438,7 +434,7 @@ class HaPanelDevStateRenderer extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"developer-tools-state-renderer": HaPanelDevStateRenderer;
"tools-state-renderer": HaPanelDevStateRenderer;
}
interface HASSDomEvents {
@@ -42,7 +42,7 @@ import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant, HomeAssistantRegistries } from "../../../../types";
import { showToast } from "../../../../util/toast";
import "./developer-tools-state-renderer";
import "./tools-state-renderer";
// Use virtualizer after threshold to avoid performance issues
// NOTE: If virtualizer is used when filtered entiity state
@@ -51,7 +51,7 @@ import "./developer-tools-state-renderer";
// virtualized list, an undesirable effect.
const VIRTUALIZE_THRESHOLD = 100;
@customElement("developer-tools-state")
@customElement("tools-state")
class HaPanelDevState extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -165,7 +165,7 @@ class HaPanelDevState extends LitElement {
<div class="heading">
<h1>
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.current_entities"
"ui.panel.config.tools.tabs.states.current_entities"
)}
</h1>
${
@@ -192,7 +192,7 @@ class HaPanelDevState extends LitElement {
@change=${this._saveAttributeCheckboxState}
>
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.attributes"
"ui.panel.config.tools.tabs.states.attributes"
)}
</ha-checkbox>
`
@@ -201,7 +201,7 @@ class HaPanelDevState extends LitElement {
</div>
<ha-expansion-panel
.header=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.set_state"
"ui.panel.config.tools.tabs.states.set_state"
)}
outlined
.expanded=${this._expanded}
@@ -209,10 +209,10 @@ class HaPanelDevState extends LitElement {
>
<p>
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.description1"
"ui.panel.config.tools.tabs.states.description1"
)}<br />
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.description2"
"ui.panel.config.tools.tabs.states.description2"
)}
</p>
${
@@ -237,7 +237,7 @@ class HaPanelDevState extends LitElement {
.path=${mdiContentCopy}
@click=${this._copyStateEntity}
title=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.copy_id"
"ui.panel.config.tools.tabs.states.copy_id"
)}
></ha-icon-button>
</div>
@@ -246,7 +246,7 @@ class HaPanelDevState extends LitElement {
}
<ha-input
.label=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.state"
"ui.panel.config.tools.tabs.states.state"
)}
required
autocapitalize="none"
@@ -259,7 +259,7 @@ class HaPanelDevState extends LitElement {
></ha-input>
<p>
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.state_attributes"
"ui.panel.config.tools.tabs.states.state_attributes"
)}
</p>
<ha-yaml-editor
@@ -274,7 +274,7 @@ class HaPanelDevState extends LitElement {
.disabled=${!this._validJSON}
raised
>${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.set_state"
"ui.panel.config.tools.tabs.states.set_state"
)}</ha-button
>
<ha-icon-button
@@ -290,7 +290,7 @@ class HaPanelDevState extends LitElement {
? html`<p>
<b
>${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.last_changed"
"ui.panel.config.tools.tabs.states.last_changed"
)}:</b
><br />
<a href=${this._historyFromLastChanged(this._entity)}
@@ -300,7 +300,7 @@ class HaPanelDevState extends LitElement {
<p>
<b
>${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.last_updated"
"ui.panel.config.tools.tabs.states.last_updated"
)}:</b
><br />
<a href=${this._historyFromLastUpdated(this._entity)}
@@ -312,7 +312,7 @@ class HaPanelDevState extends LitElement {
</div>
</div>
</ha-expansion-panel>
<developer-tools-state-renderer
<tools-state-renderer
.narrow=${this.narrow}
.entities=${entities}
.virtualize=${entities.length > VIRTUALIZE_THRESHOLD}
@@ -324,7 +324,7 @@ class HaPanelDevState extends LitElement {
<ha-input-search
slot="filter-entities"
.label=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.filter_entities"
"ui.panel.config.tools.tabs.states.filter_entities"
)}
.value=${this._entityFilter}
@input=${this._entityFilterChanged}
@@ -332,7 +332,7 @@ class HaPanelDevState extends LitElement {
<ha-input-search
slot="filter-states"
.label=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.filter_states"
"ui.panel.config.tools.tabs.states.filter_states"
)}
type="search"
.value=${this._stateFilter}
@@ -357,13 +357,13 @@ class HaPanelDevState extends LitElement {
<ha-input-search
slot="filter-attributes"
.label=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.filter_attributes"
"ui.panel.config.tools.tabs.states.filter_attributes"
)}
type="search"
.value=${this._attributeFilter}
@input=${this._attributeFilterChanged}
></ha-input-search>
</developer-tools-state-renderer>
</tools-state-renderer>
`;
}
@@ -469,7 +469,7 @@ class HaPanelDevState extends LitElement {
if (!this._entityId) {
showAlertDialog(this, {
text: this._i18n.localize(
"ui.panel.config.developer-tools.tabs.states.alert_entity_field"
"ui.panel.config.tools.tabs.states.alert_entity_field"
),
});
return;
@@ -758,6 +758,6 @@ class HaPanelDevState extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"developer-tools-state": HaPanelDevState;
"tools-state": HaPanelDevState;
}
}
@@ -132,7 +132,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
@click=${this._fetchOutliers}
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.outliers"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.outliers"
)}
</ha-button>
<ha-button slot="primaryAction" data-dialog="close">
@@ -156,7 +156,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
@click=${this._fixIssue}
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.adjust"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.adjust"
)}</ha-button
>
`;
@@ -166,7 +166,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
<ha-dialog
.open=${this._open}
header-title=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.title"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.title"
)}
.preventScrimClose=${this.isDirtyState}
@closed=${this._dialogClosed}
@@ -194,7 +194,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
} else if (this._statsHour.length < 1 && this._stats5min.length < 1) {
stats = html`<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.no_statistics_found"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.no_statistics_found"
)}
</p>`;
} else {
@@ -234,20 +234,20 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
return html`
<div class="text-content">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.info_text_1"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.info_text_1"
)}
</div>
<div class="text-content">
<b
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.statistic"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.statistic"
)}</b
>
${this._params!.statistic.statistic_id}
</div>
<ha-selector-datetime
.label=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.pick_a_time"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.pick_a_time"
)}
.hass=${this.hass}
.selector=${this._dateTimeSelector}
@@ -289,7 +289,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
<div class="text-content">
<b
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.statistic"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.statistic"
)}</b
>
${this._params!.statistic.statistic_id}
@@ -298,7 +298,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
<div class="table-row">
<span
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.start"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.start"
)}</span
>
<span
@@ -313,7 +313,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
<div class="table-row">
<span
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.end"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.end"
)}</span
>
<span
@@ -327,7 +327,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
<ha-selector-number
.label=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.new_value"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.new_value"
)}
.hass=${this.hass}
.selector=${this._amountSelector(unit || undefined, this._precision)}
@@ -506,7 +506,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
this._busy = false;
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.error_sum_adjusted",
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.error_sum_adjusted",
{ message: err.message || err }
),
});
@@ -514,7 +514,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
}
showToast(this, {
message: this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.sum_adjusted"
"ui.panel.config.tools.tabs.statistics.fix_issue.adjust_sum.sum_adjusted"
),
});
this._markDirtyStateClean();
@@ -53,13 +53,13 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
<ha-dialog
.open=${this._open}
header-title=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.title"
"ui.panel.config.tools.tabs.statistics.fix_issue.units_changed.title"
)}
@closed=${this._dialogClosed}
>
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.info_text_1",
"ui.panel.config.tools.tabs.statistics.fix_issue.units_changed.info_text_1",
{
name: getStatisticLabel(
this.hass,
@@ -72,16 +72,16 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
}
)}<br />
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.info_text_2"
"ui.panel.config.tools.tabs.statistics.fix_issue.units_changed.info_text_2"
)}<br />
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.info_text_3"
"ui.panel.config.tools.tabs.statistics.fix_issue.units_changed.info_text_3"
)}
</p>
<ha-radio-group
.label=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.how_to_fix"
"ui.panel.config.tools.tabs.statistics.fix_issue.units_changed.how_to_fix"
)}
.value=${this._action}
name="action"
@@ -89,13 +89,13 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
>
<ha-radio-option value="update" autofocus>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.update",
"ui.panel.config.tools.tabs.statistics.fix_issue.units_changed.update",
this._params.issue.data
)}
</ha-radio-option>
<ha-radio-option value="clear">
${this.hass.localize(
`ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.clear`
`ui.panel.config.tools.tabs.statistics.fix_issue.units_changed.clear`
)}
</ha-radio-option>
</ha-radio-group>
@@ -110,7 +110,7 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
</ha-button>
<ha-button slot="primaryAction" @click=${this._fixIssue}>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.fix"
"ui.panel.config.tools.tabs.statistics.fix_issue.fix"
)}
</ha-button>
</ha-dialog-footer>
@@ -50,13 +50,13 @@ export class DialogStatisticsFix extends LitElement {
<ha-dialog
.open=${this._open}
header-title=${this.hass.localize(
`ui.panel.config.developer-tools.tabs.statistics.fix_issue.${issue.type}.title`
`ui.panel.config.tools.tabs.statistics.fix_issue.${issue.type}.title`
)}
@closed=${this._dialogClosed}
>
<p>
${this.hass.localize(
`ui.panel.config.developer-tools.tabs.statistics.fix_issue.${issue.type}.info_text_1`,
`ui.panel.config.tools.tabs.statistics.fix_issue.${issue.type}.info_text_1`,
{
name: getStatisticLabel(
this.hass,
@@ -67,24 +67,24 @@ export class DialogStatisticsFix extends LitElement {
...(issue.type === "mean_type_changed"
? {
metadata_mean_type: this.hass.localize(
`ui.panel.config.developer-tools.tabs.statistics.mean_type.${issue.data.metadata_mean_type}`
`ui.panel.config.tools.tabs.statistics.mean_type.${issue.data.metadata_mean_type}`
),
state_mean_type: this.hass.localize(
`ui.panel.config.developer-tools.tabs.statistics.mean_type.${issue.data.state_mean_type}`
`ui.panel.config.tools.tabs.statistics.mean_type.${issue.data.state_mean_type}`
),
}
: {}),
}
)}<br /><br />
${this.hass.localize(
`ui.panel.config.developer-tools.tabs.statistics.fix_issue.${issue.type}.info_text_2`,
`ui.panel.config.tools.tabs.statistics.fix_issue.${issue.type}.info_text_2`,
{ statistic_id: issue.data.statistic_id }
)}
${
issue.type === "mean_type_changed"
? html`<br /><br />
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.mean_type_changed.info_text_3",
"ui.panel.config.tools.tabs.statistics.fix_issue.mean_type_changed.info_text_3",
{ statistic_id: issue.data.statistic_id }
)}`
: issue.type === "entity_not_recorded"
@@ -98,7 +98,7 @@ export class DialogStatisticsFix extends LitElement {
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_3_link"
"ui.panel.config.tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_3_link"
)}</a
>`
: issue.type === "entity_no_longer_recorded"
@@ -111,22 +111,22 @@ export class DialogStatisticsFix extends LitElement {
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link"
"ui.panel.config.tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link"
)}</a
><br /><br />
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_4"
"ui.panel.config.tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_4"
)}`
: issue.type === "state_class_removed"
? html`<ul>
<li>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_3"
"ui.panel.config.tools.tabs.statistics.fix_issue.state_class_removed.info_text_3"
)}
</li>
<li>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_4"
"ui.panel.config.tools.tabs.statistics.fix_issue.state_class_removed.info_text_4"
)}
<a
href="https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics"
@@ -134,18 +134,18 @@ export class DialogStatisticsFix extends LitElement {
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_4_link"
"ui.panel.config.tools.tabs.statistics.fix_issue.state_class_removed.info_text_4_link"
)}</a
>
</li>
<li>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_5"
"ui.panel.config.tools.tabs.statistics.fix_issue.state_class_removed.info_text_5"
)}
</li>
</ul>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_6",
"ui.panel.config.tools.tabs.statistics.fix_issue.state_class_removed.info_text_6",
{ statistic_id: issue.data.statistic_id }
)}`
: nothing
@@ -195,15 +195,15 @@ export class DialogStatisticsFix extends LitElement {
title:
err.code === "timeout"
? this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.clearing_timeout_title"
"ui.panel.config.tools.tabs.statistics.fix_issue.clearing_timeout_title"
)
: this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.clearing_failed"
"ui.panel.config.tools.tabs.statistics.fix_issue.clearing_failed"
),
text:
err.code === "timeout"
? this.hass.localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.clearing_timeout_text"
"ui.panel.config.tools.tabs.statistics.fix_issue.clearing_timeout_text"
)
: err.message,
});
@@ -98,7 +98,7 @@ type DisplayedStatisticData = StatisticData & {
issues_string?: string;
};
@customElement("developer-tools-statistics")
@customElement("tools-statistics")
class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
@property({ type: Boolean, reflect: true }) public narrow = false;
@@ -188,7 +188,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
?.map(
(issue) =>
localize(
`ui.panel.config.developer-tools.tabs.statistics.issues.${issue.type}`,
`ui.panel.config.tools.tabs.statistics.issues.${issue.type}`,
issue.data
) || issue.type
)
@@ -204,7 +204,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
): DataTableColumnContainer<DisplayedStatisticData> => ({
displayName: {
title: localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.name"
"ui.panel.config.tools.tabs.statistics.data_table.name"
),
main: true,
sortable: true,
@@ -225,7 +225,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
area: getAreaTableColumn(localize),
statistic_id: {
title: localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.statistic_id"
"ui.panel.config.tools.tabs.statistics.data_table.statistic_id"
),
sortable: true,
filterable: true,
@@ -233,7 +233,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
},
statistics_unit_of_measurement: {
title: localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.statistics_unit"
"ui.panel.config.tools.tabs.statistics.data_table.statistics_unit"
),
sortable: true,
filterable: true,
@@ -241,7 +241,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
},
source: {
title: localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.source"
"ui.panel.config.tools.tabs.statistics.data_table.source"
),
sortable: true,
filterable: true,
@@ -249,7 +249,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
},
issues_string: {
title: localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.issue"
"ui.panel.config.tools.tabs.statistics.data_table.issue"
),
sortable: true,
filterable: true,
@@ -259,14 +259,12 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
template: (statistic) =>
html`${
statistic.issues_string ??
localize("ui.panel.config.developer-tools.tabs.statistics.no_issue")
localize("ui.panel.config.tools.tabs.statistics.no_issue")
}`,
},
fix: {
title: "",
label: localize(
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.fix"
),
label: localize("ui.panel.config.tools.tabs.statistics.fix_issue.fix"),
type: "icon",
template: (statistic) =>
html`${
@@ -281,8 +279,8 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
statistic.issues.some((issue) =>
FIXABLE_ISSUES.includes(issue.type)
)
? "ui.panel.config.developer-tools.tabs.statistics.fix_issue.fix"
: "ui.panel.config.developer-tools.tabs.statistics.fix_issue.info"
? "ui.panel.config.tools.tabs.statistics.fix_issue.fix"
: "ui.panel.config.tools.tabs.statistics.fix_issue.info"
)}
</ha-button>`
: "—"
@@ -293,9 +291,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
},
actions: {
title: "",
label: localize(
"ui.panel.config.developer-tools.tabs.statistics.adjust_sum"
),
label: localize("ui.panel.config.tools.tabs.statistics.adjust_sum"),
type: "icon-button",
showNarrow: true,
template: (statistic) =>
@@ -303,7 +299,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
? html`
<ha-icon-button
.label=${localize(
"ui.panel.config.developer-tools.tabs.statistics.adjust_sum"
"ui.panel.config.tools.tabs.statistics.adjust_sum"
)}
.path=${mdiSlopeUphill}
.statistic=${statistic}
@@ -500,7 +496,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
</ha-dropdown-item>
<ha-dropdown-item @click=${this._selectAllIssues}>
${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.select_all_issues"
"ui.panel.config.tools.tabs.statistics.data_table.select_all_issues"
)}
</ha-dropdown-item>
<ha-dropdown-item @click=${this._selectNone}>
@@ -529,7 +525,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
</div>
<ha-assist-chip
.label=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.statistics.delete_selected"
"ui.panel.config.tools.tabs.statistics.delete_selected"
)}
.disabled=${!this._selected.length}
@click=${this._clearSelected}
@@ -563,7 +559,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
this._registries.areas
)}
.noDataText=${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.statistics.data_table.no_statistics"
"ui.panel.config.tools.tabs.statistics.data_table.no_statistics"
)}
.filter=${this.filter}
.selectable=${this._selectMode}
@@ -770,10 +766,10 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
await showConfirmationDialog(this, {
title: this._i18n.localize(
"ui.panel.config.developer-tools.tabs.statistics.multi_delete.title"
"ui.panel.config.tools.tabs.statistics.multi_delete.title"
),
text: html`${this._i18n.localize(
"ui.panel.config.developer-tools.tabs.statistics.multi_delete.info_text",
"ui.panel.config.tools.tabs.statistics.multi_delete.info_text",
{ statistic_count: deletableIds.length }
)}`,
confirmText: this._i18n.localize("ui.common.delete"),
@@ -925,6 +921,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
declare global {
interface HTMLElementTagNameMap {
"developer-tools-statistics": HaPanelDevStatistics;
"tools-statistics": HaPanelDevStatistics;
}
}
@@ -4,6 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import { debounce } from "../../../../common/util/debounce";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
@@ -40,7 +41,16 @@ For loop example getting entity values in the weather domain:
{{ state.name | lower }} is {{state.state_with_unit}}
{%- endfor %}.`;
@customElement("developer-tools-template")
// key resolves the label/description translation keys; path is passed through
// documentationUrl().
const TEMPLATE_DOCS_LINKS: { key: string; path: string }[] = [
{ key: "docs_introduction", path: "/docs/templating/introduction/" },
{ key: "docs_states", path: "/docs/templating/states/" },
{ key: "docs_debugging", path: "/docs/templating/debugging/" },
{ key: "docs_functions", path: "/template-functions/" },
];
@customElement("tools-template")
class HaPanelDevTemplate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -108,7 +118,7 @@ class HaPanelDevTemplate extends LitElement {
<div class="content">
<ha-expansion-panel
.header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.about"
"ui.panel.config.tools.tabs.templates.about"
)}
outlined
.expanded=${this._descriptionExpanded}
@@ -117,34 +127,39 @@ class HaPanelDevTemplate extends LitElement {
<div class="description">
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.description"
"ui.panel.config.tools.tabs.templates.description"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.tools.tabs.templates.engine_info"
)}
</p>
<h3>
${this.hass.localize(
"ui.panel.config.tools.tabs.templates.learn_more"
)}
</h3>
<ul>
<li>
<a
href="https://jinja.palletsprojects.com/en/latest/templates/"
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.jinja_documentation"
)}
</a>
</li>
<li>
<a
href=${documentationUrl(
this.hass,
"/docs/configuration/templating/"
)}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.template_extensions"
)}</a
>
</li>
${TEMPLATE_DOCS_LINKS.map(
(link) => html`
<li>
<a
href=${documentationUrl(this.hass, link.path)}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
`ui.panel.config.tools.tabs.templates.${link.key}` as LocalizeKeys
)}</a
>
<span class="link-description"
>${this.hass.localize(
`ui.panel.config.tools.tabs.templates.${link.key}_description` as LocalizeKeys
)}</span
>
</li>
`
)}
</ul>
</div>
</ha-expansion-panel>
@@ -159,7 +174,7 @@ class HaPanelDevTemplate extends LitElement {
<ha-card
class="edit-pane"
header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.editor"
"ui.panel.config.tools.tabs.templates.editor"
)}
>
<div class="card-content">
@@ -177,7 +192,7 @@ class HaPanelDevTemplate extends LitElement {
<div class="card-actions">
<ha-button appearance="plain" @click=${this._restoreDemo}>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.reset"
"ui.panel.config.tools.tabs.templates.reset"
)}
</ha-button>
<ha-button appearance="plain" @click=${this._clear}>
@@ -186,7 +201,7 @@ class HaPanelDevTemplate extends LitElement {
</div>
<ha-tip>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.keyboard_tip",
"ui.panel.config.tools.tabs.templates.keyboard_tip",
{
autocomplete: html`<kbd>Ctrl</kbd>+<kbd>Space</kbd>`,
}
@@ -197,7 +212,7 @@ class HaPanelDevTemplate extends LitElement {
<ha-card
class="render-pane"
header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.result"
"ui.panel.config.tools.tabs.templates.result"
)}
>
<div class="card-content ha-scrollbar">
@@ -231,7 +246,7 @@ ${
}</pre>
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.result_type"
"ui.panel.config.tools.tabs.templates.result_type"
)}:
${resultType}
</p>
@@ -240,7 +255,7 @@ ${
? html`
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.time"
"ui.panel.config.tools.tabs.templates.time"
)}
</p>
`
@@ -253,7 +268,7 @@ ${
? html`
<p class="all_listeners">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.all_listeners"
"ui.panel.config.tools.tabs.templates.all_listeners"
)}
</p>
`
@@ -262,7 +277,7 @@ ${
? html`
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.listeners"
"ui.panel.config.tools.tabs.templates.listeners"
)}
</p>
<ul>
@@ -273,7 +288,7 @@ ${
<li>
<b
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.domain"
"ui.panel.config.tools.tabs.templates.domain"
)}</b
>: ${domain}
</li>
@@ -286,7 +301,7 @@ ${
<li>
<b
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.entity"
"ui.panel.config.tools.tabs.templates.entity"
)}</b
>: ${entity_id}
</li>
@@ -297,7 +312,7 @@ ${
: !this._templateResult.listeners.time
? html`<span class="all_listeners">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.no_listeners"
"ui.panel.config.tools.tabs.templates.no_listeners"
)}
</span>`
: nothing
@@ -441,6 +456,17 @@ ${
margin-block-start: var(--ha-space-1);
margin-block-end: var(--ha-space-1);
}
.description > h3 {
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
margin-block-end: var(--ha-space-1);
}
.description li {
margin-block-end: var(--ha-space-1);
}
.description .link-description {
color: var(--secondary-text-color);
}
.render-pane .card-content {
user-select: text;
@@ -596,7 +622,7 @@ ${
if (
!(await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.confirm_reset"
"ui.panel.config.tools.tabs.templates.confirm_reset"
),
warning: true,
}))
@@ -612,7 +638,7 @@ ${
if (
!(await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.developer-tools.tabs.templates.confirm_clear"
"ui.panel.config.tools.tabs.templates.confirm_clear"
),
warning: true,
}))
@@ -632,6 +658,6 @@ ${
declare global {
interface HTMLElementTagNameMap {
"developer-tools-template": HaPanelDevTemplate;
"tools-template": HaPanelDevTemplate;
}
}
@@ -3,8 +3,8 @@ import type { RouterOptions } from "../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../layouts/hass-router-page";
import type { HomeAssistant } from "../../../types";
@customElement("developer-tools-router")
class DeveloperToolsRouter extends HassRouterPage {
@customElement("tools-router")
class ToolsRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@@ -22,37 +22,37 @@ class DeveloperToolsRouter extends HassRouterPage {
showLoading: true,
routes: {
event: {
tag: "developer-tools-event",
load: () => import("./event/developer-tools-event"),
tag: "tools-event",
load: () => import("./event/tools-event"),
},
service: "action",
action: {
tag: "developer-tools-action",
load: () => import("./action/developer-tools-action"),
tag: "tools-action",
load: () => import("./action/tools-action"),
},
state: {
tag: "developer-tools-state",
load: () => import("./state/developer-tools-state"),
tag: "tools-state",
load: () => import("./state/tools-state"),
},
template: {
tag: "developer-tools-template",
load: () => import("./template/developer-tools-template"),
tag: "tools-template",
load: () => import("./template/tools-template"),
},
statistics: {
tag: "developer-tools-statistics",
load: () => import("./statistics/developer-tools-statistics"),
tag: "tools-statistics",
load: () => import("./statistics/tools-statistics"),
},
yaml: {
tag: "developer-yaml-config",
load: () => import("./yaml_configuration/developer-yaml-config"),
tag: "tools-yaml-config",
load: () => import("./yaml_configuration/tools-yaml-config"),
},
assist: {
tag: "developer-tools-assist",
load: () => import("./assist/developer-tools-assist"),
tag: "tools-assist",
load: () => import("./assist/tools-assist"),
},
debug: {
tag: "developer-tools-debug",
load: () => import("./debug/developer-tools-debug"),
tag: "tools-debug",
load: () => import("./debug/tools-debug"),
},
},
};
@@ -77,6 +77,6 @@ class DeveloperToolsRouter extends HassRouterPage {
declare global {
interface HTMLElementTagNameMap {
"developer-tools-router": DeveloperToolsRouter;
"tools-router": ToolsRouter;
}
}
@@ -16,7 +16,7 @@ import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant, Route, TranslationDict } from "../../../../types";
type ReloadableDomain = Exclude<
keyof TranslationDict["ui"]["panel"]["config"]["developer-tools"]["tabs"]["yaml"]["section"]["reloading"],
keyof TranslationDict["ui"]["panel"]["config"]["tools"]["tabs"]["yaml"]["section"]["reloading"],
"heading" | "introduction" | "reload"
>;
@@ -25,8 +25,8 @@ interface TranslatedReloadableDomain {
name: string;
}
@customElement("developer-yaml-config")
export class DeveloperYamlConfig extends LitElement {
@customElement("tools-yaml-config")
export class ToolsYamlConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@@ -61,10 +61,10 @@ export class DeveloperYamlConfig extends LitElement {
domain,
name:
this.hass.localize(
`ui.panel.config.developer-tools.tabs.yaml.section.reloading.${domain}`
`ui.panel.config.tools.tabs.yaml.section.reloading.${domain}`
) ||
this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.reloading.reload",
"ui.panel.config.tools.tabs.yaml.section.reloading.reload",
{ domain: domainToName(this.hass.localize, domain) }
),
}))
@@ -80,12 +80,12 @@ export class DeveloperYamlConfig extends LitElement {
<ha-card
outlined
header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.validation.heading"
"ui.panel.config.tools.tabs.yaml.section.validation.heading"
)}
>
<div class="card-content">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.validation.introduction"
"ui.panel.config.tools.tabs.yaml.section.validation.introduction"
)}
${
!this._validateResult
@@ -103,10 +103,10 @@ export class DeveloperYamlConfig extends LitElement {
${
this._validateResult.result === "valid"
? this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.validation.valid"
"ui.panel.config.tools.tabs.yaml.section.validation.valid"
)
: this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.validation.invalid"
"ui.panel.config.tools.tabs.yaml.section.validation.invalid"
)
}
</div>
@@ -116,7 +116,7 @@ export class DeveloperYamlConfig extends LitElement {
? html`<ha-alert
alert-type="error"
.title=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.validation.errors"
"ui.panel.config.tools.tabs.yaml.section.validation.errors"
)}
>
<!-- prettier-ignore -->
@@ -131,7 +131,7 @@ export class DeveloperYamlConfig extends LitElement {
? html`<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.validation.warnings"
"ui.panel.config.tools.tabs.yaml.section.validation.warnings"
)}
>
<!-- prettier-ignore -->
@@ -148,7 +148,7 @@ export class DeveloperYamlConfig extends LitElement {
<div class="card-actions">
<ha-button appearance="plain" @click=${this._validateConfig}>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.validation.check_config"
"ui.panel.config.tools.tabs.yaml.section.validation.check_config"
)}
</ha-button>
<ha-button
@@ -158,7 +158,7 @@ export class DeveloperYamlConfig extends LitElement {
.disabled=${this._validateResult?.result === "invalid"}
>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.server_management.restart"
"ui.panel.config.tools.tabs.yaml.section.server_management.restart"
)}
</ha-button>
</div>
@@ -166,18 +166,18 @@ export class DeveloperYamlConfig extends LitElement {
<ha-card
outlined
header=${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.reloading.heading"
"ui.panel.config.tools.tabs.yaml.section.reloading.heading"
)}
>
<div class="card-content">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.reloading.introduction"
"ui.panel.config.tools.tabs.yaml.section.reloading.introduction"
)}
</div>
<div class="card-actions">
<ha-call-service-button domain="homeassistant" service="reload_all"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.reloading.all"
"ui.panel.config.tools.tabs.yaml.section.reloading.all"
)}
</ha-call-service-button>
</div>
@@ -186,7 +186,7 @@ export class DeveloperYamlConfig extends LitElement {
domain="homeassistant"
service="reload_core_config"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.yaml.section.reloading.core"
"ui.panel.config.tools.tabs.yaml.section.reloading.core"
)}
</ha-call-service-button>
</div>
@@ -269,6 +269,6 @@ export class DeveloperYamlConfig extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"developer-yaml-config": DeveloperYamlConfig;
"tools-yaml-config": ToolsYamlConfig;
}
}
@@ -12,14 +12,12 @@ import {
startOfMonth,
addYears,
addMonths,
addMinutes,
addHours,
startOfDay,
addDays,
subDays,
} from "date-fns";
import type {
BarSeriesOption,
CallbackDataParams,
LineSeriesOption,
TopLevelFormatterParams,
@@ -379,97 +377,12 @@ const PERIOD_MS: Record<string, number> = {
/**
* Offset from a period's start to its midpoint, for centering sub-daily bars
* (and forecast lines) between axis ticks — 0 for daily+ periods, which sit at
* the start.
*
* `measuredGap` is the gap between the first two entries, when available. It
* adapts the offset to data that is finer-grained than the nominal period
* (e.g. external forecast data), but is clamped to the nominal period so
* sparse data (gaps between readings) can't inflate the offset, and a lone
* bucket (no gap to measure) still centers on the nominal midpoint.
* the start. Derived from the period, not from the data, so the first/only
* bucket centers identically to every other bucket. (Previously estimated from
* the gap between the first two entries, which collapsed to 0 with one bucket.)
*/
export function getPeriodMidpointOffset(
period: string,
measuredGap?: number
): number {
const nominal = PERIOD_MS[period] ?? 0;
return (measuredGap ? Math.min(measuredGap, nominal) : nominal) / 2;
}
/**
* Generate the expected statistics-bucket grid across [start, end) so sparse
* data can be zero-filled. Without a dense grid, ECharts derives the bar band
* width from the minimum gap between data points: sparse data yields
* oversized bars, and a single point makes ECharts expand the time axis by
* ±40% of its span, ignoring the configured min/max.
*
* The grid is anchored on the first real data bucket of a non-compare bar
* series rather than on `start`: recorder buckets are UTC-aligned, so in
* half-hour timezones they don't sit on local period boundaries. Stepping
* from a real bucket keeps generated buckets exactly on the data's grid
* (midpoints for sub-daily periods, period starts otherwise). Returns an
* empty array when there is no data to anchor on.
*/
export function generateFillBuckets(
datasets: BarSeriesOption[],
start: Date,
end: Date,
period: "5minute" | "hour" | "day" | "month"
): number[] {
let anchor: number | undefined;
for (const dataset of datasets) {
if (
dataset.type !== "bar" ||
String(dataset.id).startsWith("compare-") ||
!dataset.data?.length
) {
continue;
}
const first = dataset.data[0];
const value =
first && typeof first === "object" && "value" in first
? first.value
: first;
const x = Number((value as number[])?.[0]);
if (!Number.isNaN(x)) {
anchor = x;
break;
}
}
if (anchor === undefined) {
return [];
}
const anchorDate = new Date(anchor);
// Step relative to the anchor (not iteratively) so month-length clamping
// and DST shifts can't accumulate drift.
const bucketAt = (n: number): number =>
(period === "5minute"
? addMinutes(anchorDate, 5 * n)
: period === "hour"
? addHours(anchorDate, n)
: period === "day"
? addDays(anchorDate, n)
: addMonths(anchorDate, n)
).getTime();
const startMs = start.getTime();
const endMs = end.getTime();
const buckets: number[] = [];
for (let n = 0; ; n--) {
const ts = bucketAt(n);
if (ts < startMs) {
break;
}
buckets.push(ts);
}
for (let n = 1; ; n++) {
const ts = bucketAt(n);
if (ts >= endMs) {
break;
}
buckets.push(ts);
}
return buckets;
export function getPeriodMidpointOffset(period: string): number {
return (PERIOD_MS[period] ?? 0) / 2;
}
export interface UntrackedSplit {
@@ -23,7 +23,6 @@ import {
computeStatMidpoint,
type EnergyDataPoint,
fillDataGapsAndRoundCaps,
generateFillBuckets,
getCompareTransform,
getPeriodMidpointOffset,
splitUntrackedConsumption,
@@ -239,15 +238,15 @@ function processUntracked(
const sortedTimes = Object.keys(consumptionData.used_total).sort(
(a, b) => Number(a) - Number(b)
);
// Only start timestamps available here, so center sub-daily bars from the
// gap between the first two entries, clamped to the nominal period so
// sparse or lone buckets stay centered on the same grid as the device bars.
const periodOffset = getPeriodMidpointOffset(
period,
sortedTimes.length >= 2
? Number(sortedTimes[1]) - Number(sortedTimes[0])
: undefined
);
// Only start timestamps available here, so center sub-daily bars using the
// gap between the first two entries. With a lone first-of-day bucket there is
// no gap to measure, so fall back to the nominal period midpoint — which
// matches the device bars' computeStatMidpoint instead of collapsing to the
// period start and splitting into a second stack.
const periodOffset =
(period === "hour" || period === "5minute") && sortedTimes.length >= 2
? (Number(sortedTimes[1]) - Number(sortedTimes[0])) / 2
: getPeriodMidpointOffset(period);
sortedTimes.forEach((time) => {
const ts = Number(time);
const x = compare
@@ -516,11 +515,7 @@ export function generateEnergyDevicesDetailGraphData(
}
}
fillDataGapsAndRoundCaps(
datasets,
true,
generateFillBuckets(datasets, start, end, getSuggestedPeriod(start, end))
);
fillDataGapsAndRoundCaps(datasets);
const yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
return {
@@ -12,7 +12,6 @@ import type { HomeAssistant } from "../../../../types";
import { getEnergyColor } from "./common/color";
import {
type EnergyDataPoint,
generateFillBuckets,
getCompareTransform,
} from "./common/energy-chart-options";
@@ -114,11 +113,7 @@ export function generateEnergyGasGraphData(
)
);
fillDataGapsAndRoundCaps(
datasets,
true,
generateFillBuckets(datasets, start, end, period)
);
fillDataGapsAndRoundCaps(datasets);
const yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
const chartData = datasets;
const total = processTotal(energyData.stats, gasSources);
@@ -13,7 +13,6 @@ import { getEnergyColor } from "./common/color";
import {
type EnergyDataPoint,
fillDataGapsAndRoundCaps,
generateFillBuckets,
getCompareTransform,
getPeriodMidpointOffset,
} from "./common/energy-chart-options";
@@ -118,11 +117,7 @@ export function generateEnergySolarGraphData(
)
);
fillDataGapsAndRoundCaps(
datasets as BarSeriesOption[],
true,
generateFillBuckets(datasets as BarSeriesOption[], start, end, period)
);
fillDataGapsAndRoundCaps(datasets as BarSeriesOption[]);
if (forecasts) {
datasets.push(
@@ -327,18 +322,20 @@ function processForecast(
if (forecastsData) {
const solarForecastData: LineSeriesOption["data"] = [];
// Center forecast points for sub-daily periods from the gap between
// the first two entries, clamped to the nominal period so sparse or
// lone forecast buckets still align with the bars.
const forecastTimes = Object.keys(forecastsData)
.map(Number)
.sort((a, b) => a - b);
const forecastOffset = getPeriodMidpointOffset(
period,
forecastTimes.length >= 2
? forecastTimes[1] - forecastTimes[0]
: undefined
);
// Only center forecast points for sub-daily periods to align with bars.
// Only start timestamps available, so estimate midpoint from the gap
// between the first two entries; with a lone first bucket there is no
// gap to measure, so fall back to the nominal period midpoint.
let forecastOffset = 0;
if (period === "hour" || period === "5minute") {
const forecastTimes = Object.keys(forecastsData)
.map(Number)
.sort((a, b) => a - b);
forecastOffset =
forecastTimes.length >= 2
? (forecastTimes[1] - forecastTimes[0]) / 2
: getPeriodMidpointOffset(period);
}
for (const [time, value] of Object.entries(forecastsData)) {
const kWh = value / 1000;
solarForecastData.push([Number(time) + forecastOffset, kWh]);
@@ -37,7 +37,6 @@ import { hasConfigChanged } from "../../common/has-changed";
import {
type EnergyDataPoint,
fillDataGapsAndRoundCaps,
generateFillBuckets,
getCommonOptions,
getCompareTransform,
getPeriodMidpointOffset,
@@ -451,16 +450,7 @@ export class HuiEnergyUsageGraphCard
// @ts-expect-error
datasets.sort((a, b) => a.order - b.order);
fillDataGapsAndRoundCaps(
datasets,
true,
generateFillBuckets(
datasets,
this._start,
this._end,
getSuggestedPeriod(this._start, this._end)
)
);
fillDataGapsAndRoundCaps(datasets);
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
this._chartData = datasets;
this._legendData = this._getLegendData(datasets);
@@ -606,14 +596,15 @@ export class HuiEnergyUsageGraphCard
const uniqueKeys = summedData.timestamps;
// Only start timestamps available here, so center sub-daily bars from the
// gap between the first two entries, clamped to the nominal period so
// sparse or lone buckets stay centered on the same grid as dense data.
// Only center bars for sub-daily periods (hour/5min). Only start timestamps
// available here, so estimate midpoint from the gap between the first two
// entries; with a lone first-of-day bucket there is no gap to measure, so
// fall back to the nominal period midpoint so the bar stays centered.
const period = getSuggestedPeriod(this._start, this._end);
const periodOffset = getPeriodMidpointOffset(
period,
uniqueKeys.length >= 2 ? uniqueKeys[1] - uniqueKeys[0] : undefined
);
const periodOffset =
(period === "hour" || period === "5minute") && uniqueKeys.length >= 2
? (uniqueKeys[1] - uniqueKeys[0]) / 2
: getPeriodMidpointOffset(period);
const compareTransform = getCompareTransform(
this._start,
@@ -31,7 +31,6 @@ import {
computeStatMidpoint,
type EnergyDataPoint,
fillDataGapsAndRoundCaps,
generateFillBuckets,
getCommonOptions,
getCompareTransform,
} from "./common/energy-chart-options";
@@ -264,16 +263,7 @@ export class HuiEnergyWaterGraphCard
)
);
fillDataGapsAndRoundCaps(
datasets,
true,
generateFillBuckets(
datasets,
this._start,
this._end,
getSuggestedPeriod(this._start, this._end)
)
);
fillDataGapsAndRoundCaps(datasets);
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
this._chartData = datasets;
this._total = this._processTotal(energyData.stats, waterSources);
+39 -9
View File
@@ -25,35 +25,65 @@ export const getMyRedirects = (): Redirects => ({
application_credentials: {
redirect: "/config/application_credentials",
},
tools_assist: {
redirect: "/config/tools/assist",
},
tools_debug: {
redirect: "/config/tools/debug",
},
tools_states: {
redirect: "/config/tools/state",
},
tools_actions: {
redirect: "/config/tools/action",
},
tools_perform_action: {
redirect: "/config/tools/action",
params: {
service: "string",
},
},
tools_template: {
redirect: "/config/tools/template",
},
tools_events: {
redirect: "/config/tools/event",
},
tools_statistics: {
redirect: "/config/tools/statistics",
},
tools_yaml: {
redirect: "/config/tools/yaml",
},
developer_assist: {
redirect: "/config/developer-tools/assist",
redirect: "/config/tools/assist",
},
developer_debug: {
redirect: "/config/developer-tools/debug",
redirect: "/config/tools/debug",
},
developer_states: {
redirect: "/config/developer-tools/state",
redirect: "/config/tools/state",
},
developer_services: {
redirect: "/config/developer-tools/action",
redirect: "/config/tools/action",
},
developer_call_service: {
redirect: "/config/developer-tools/action",
redirect: "/config/tools/action",
params: {
service: "string",
},
},
developer_template: {
redirect: "/config/developer-tools/template",
redirect: "/config/tools/template",
},
developer_events: {
redirect: "/config/developer-tools/event",
redirect: "/config/tools/event",
},
developer_statistics: {
redirect: "/config/developer-tools/statistics",
redirect: "/config/tools/statistics",
},
server_controls: {
redirect: "/config/developer-tools/yaml",
redirect: "/config/tools/yaml",
},
calendar: {
component: "calendar",
+62 -54
View File
@@ -1459,7 +1459,7 @@
"automations": "[%key:ui::panel::config::automation::caption%]",
"scenes": "[%key:ui::panel::config::scene::caption%]",
"scripts": "[%key:ui::panel::config::script::caption%]",
"developer_tools": "[%key:ui::panel::config::dashboard::developer_tools::main%]",
"tools": "[%key:ui::panel::config::dashboard::tools::main%]",
"integrations": "[%key:ui::panel::config::integrations::caption%]",
"devices": "[%key:ui::panel::config::devices::caption%]",
"entities": "[%key:ui::panel::config::entities::caption%]"
@@ -1485,45 +1485,45 @@
"navigate_title": "Navigate",
"commands": {
"reload": {
"all": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::all%]",
"reload": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::reload%]",
"core": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::core%]",
"group": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::group%]",
"automation": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::automation%]",
"script": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::script%]",
"scene": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::scene%]",
"person": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::person%]",
"zone": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::zone%]",
"input_boolean": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_boolean%]",
"input_button": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_button%]",
"input_text": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_text%]",
"input_number": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_number%]",
"input_datetime": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_datetime%]",
"input_select": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_select%]",
"template": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::template%]",
"universal": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::universal%]",
"rest": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::rest%]",
"command_line": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::command_line%]",
"filter": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::filter%]",
"statistics": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::statistics%]",
"generic": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::generic%]",
"generic_thermostat": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::generic_thermostat%]",
"homekit": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::homekit%]",
"min_max": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::min_max%]",
"history_stats": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::history_stats%]",
"trend": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::trend%]",
"ping": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::ping%]",
"filesize": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::filesize%]",
"telegram": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::telegram%]",
"smtp": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::smtp%]",
"mqtt": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::mqtt%]",
"rpi_gpio": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::rpi_gpio%]",
"themes": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::themes%]"
"all": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::all%]",
"reload": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::reload%]",
"core": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::core%]",
"group": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::group%]",
"automation": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::automation%]",
"script": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::script%]",
"scene": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::scene%]",
"person": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::person%]",
"zone": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::zone%]",
"input_boolean": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::input_boolean%]",
"input_button": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::input_button%]",
"input_text": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::input_text%]",
"input_number": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::input_number%]",
"input_datetime": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::input_datetime%]",
"input_select": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::input_select%]",
"template": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::template%]",
"universal": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::universal%]",
"rest": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::rest%]",
"command_line": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::command_line%]",
"filter": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::filter%]",
"statistics": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::statistics%]",
"generic": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::generic%]",
"generic_thermostat": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::generic_thermostat%]",
"homekit": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::homekit%]",
"min_max": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::min_max%]",
"history_stats": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::history_stats%]",
"trend": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::trend%]",
"ping": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::ping%]",
"filesize": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::filesize%]",
"telegram": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::telegram%]",
"smtp": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::smtp%]",
"mqtt": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::mqtt%]",
"rpi_gpio": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::rpi_gpio%]",
"themes": "[%key:ui::panel::config::tools::tabs::yaml::section::reloading::themes%]"
},
"home_assistant_control": {
"perform_action": "{action} Home Assistant",
"restart": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::server_management::restart%]",
"stop": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::server_management::stop%]"
"restart": "[%key:ui::panel::config::tools::tabs::yaml::section::server_management::restart%]",
"stop": "[%key:ui::panel::config::tools::tabs::yaml::section::server_management::stop%]"
},
"types": {
"reload": "Reload",
@@ -1559,14 +1559,14 @@
"analytics": "[%key:ui::panel::config::analytics::caption%]",
"system_health": "[%key:ui::panel::config::system_health::caption%]",
"blueprint": "[%key:ui::panel::config::blueprint::caption%]",
"server_control": "[%key:ui::panel::config::developer-tools::tabs::yaml::title%]",
"server_control": "[%key:ui::panel::config::tools::tabs::yaml::title%]",
"system": "[%key:ui::panel::config::dashboard::system::main%]",
"apps": "Apps",
"app_store": "App store",
"app_info": "{app} info",
"shortcuts": "[%key:ui::panel::config::info::shortcuts%]",
"labs": "[%key:ui::panel::config::labs::caption%]",
"developer-tools": "[%key:ui::panel::config::dashboard::developer_tools::main%]",
"tools": "[%key:ui::panel::config::dashboard::tools::main%]",
"matter": "[%key:ui::panel::config::dashboard::matter::main%]",
"zha": "[%key:ui::panel::config::dashboard::zha::main%]",
"zwave_js": "[%key:ui::panel::config::dashboard::zwave_js::main%]",
@@ -2154,12 +2154,12 @@
"data": "Additional data"
},
"template": {
"time": "[%key:ui::panel::config::developer-tools::tabs::templates::time%]",
"all_listeners": "[%key:ui::panel::config::developer-tools::tabs::templates::all_listeners%]",
"no_listeners": "[%key:ui::panel::config::developer-tools::tabs::templates::no_listeners%]",
"listeners": "[%key:ui::panel::config::developer-tools::tabs::templates::listeners%]",
"entity": "[%key:ui::panel::config::developer-tools::tabs::templates::entity%]",
"domain": "[%key:ui::panel::config::developer-tools::tabs::templates::domain%]"
"time": "[%key:ui::panel::config::tools::tabs::templates::time%]",
"all_listeners": "[%key:ui::panel::config::tools::tabs::templates::all_listeners%]",
"no_listeners": "[%key:ui::panel::config::tools::tabs::templates::no_listeners%]",
"listeners": "[%key:ui::panel::config::tools::tabs::templates::listeners%]",
"entity": "[%key:ui::panel::config::tools::tabs::templates::entity%]",
"domain": "[%key:ui::panel::config::tools::tabs::templates::domain%]"
}
},
"options_flow": {
@@ -2638,9 +2638,9 @@
"main": "System",
"secondary": "Create backups, check logs, or reboot your system"
},
"developer_tools": {
"main": "Developer tools",
"secondary": "Tools to inspect and debug your system"
"tools": {
"main": "Tools",
"secondary": "Inspect and debug your system"
},
"about": {
"main": "About",
@@ -3786,7 +3786,7 @@
"companion_apps": "Companion apps"
}
},
"developer-tools": {
"tools": {
"tabs": {
"assist": {
"tab": "Assist",
@@ -3904,7 +3904,9 @@
},
"templates": {
"title": "Template",
"description": "Templates are rendered using the Jinja2 template engine with some Home Assistant specific extensions.",
"description": "Templates let you generate dynamic content from your Home Assistant data, such as a notification that lists which lights are on, or a sensor whose value is calculated from several other entities.",
"engine_info": "Home Assistant uses the Jinja templating engine, extended with functions for working with your entities, areas, devices, and more. Write a template in the editor below and its result updates live as your states change.",
"learn_more": "Learn more",
"about": "About templates",
"editor": "Template editor",
"result": "Result",
@@ -3912,8 +3914,14 @@
"confirm_reset": "Do you want to reset your current template back to the demo template?",
"confirm_clear": "Do you want to clear your current template?",
"result_type": "Result type",
"jinja_documentation": "Jinja2 template documentation",
"template_extensions": "Home Assistant template extensions",
"docs_introduction": "Introduction to templating",
"docs_introduction_description": "Start here for a step-by-step guide.",
"docs_states": "Working with states",
"docs_states_description": "Read entity states and attributes in templates.",
"docs_debugging": "Debugging templates",
"docs_debugging_description": "Find and fix problems in your templates.",
"docs_functions": "Template functions reference",
"docs_functions_description": "Search every available function, filter, and test.",
"unknown_error_template": "Unknown error rendering template",
"time": "This template updates at the start of each minute.",
"all_listeners": "This template listens for all state changed events.",
@@ -4819,7 +4827,7 @@
"run_text_pipeline": "Run text pipeline",
"run_audio_pipeline": "Run audio pipeline",
"run_audio_with_wake": "Run audio pipeline with wake word detection",
"response": "[%key:ui::panel::config::developer-tools::tabs::actions::response%]",
"response": "[%key:ui::panel::config::tools::tabs::actions::response%]",
"send": "Send",
"continue_listening": "Continue listening for wake word",
"continue_talking": "Continue talking",
@@ -5120,7 +5128,7 @@
"type_script_plural": "[%key:ui::panel::config::blueprint::overview::types_plural::script%]",
"type_scene_plural": "scenes",
"new_automation_setup_failed_title": "New {type} setup timed out",
"new_automation_setup_failed_text": "Your new {type} was saved, but waiting for it to set up has timed out. This could be due to errors parsing your configuration.yaml, please check the configuration in developer tools. Your {type} will not be visible until this is corrected and {types} are reloaded. Changes to area, category, or labels were not saved and must be reapplied.",
"new_automation_setup_failed_text": "Your new {type} was saved, but waiting for it to set up has timed out. This could be due to errors parsing your configuration.yaml, please check the configuration in the Tools panel. Your {type} will not be visible until this is corrected and {types} are reloaded. Changes to area, category, or labels were not saved and must be reapplied.",
"new_automation_setup_keep_waiting": "You may continue to wait for a response from the server, in case it is just taking an unusually long time to process this {type}.",
"new_automation_setup_timedout_success": "The server has responded and this has now set up successfully. You may now close this dialog.",
"item_pasted": "{item} pasted",
+148 -9
View File
@@ -99,7 +99,7 @@ test.describe("App shell", () => {
await goToPanel(page, "/lovelace");
// Regular panels use #sidebar-panel-{urlPath} inside ha-sidebar's shadow root
for (const urlPath of ["lovelace", "energy", "history"]) {
for (const urlPath of ["lovelace", "map", "energy", "history"]) {
// eslint-disable-next-line no-await-in-loop
await expect(
page.locator(
@@ -115,6 +115,71 @@ test.describe("App shell", () => {
).toBeAttached();
});
test("sidebar navigation changes the active panel", async ({ page }) => {
await goToPanel(page, "/lovelace");
const sidebar = page.locator(
"ha-test >> home-assistant-main >> ha-sidebar"
);
await expect(sidebar).toBeAttached({ timeout: SHELL_TIMEOUT });
const historyLink = sidebar.locator("#sidebar-panel-history");
if (!(await historyLink.isVisible().catch(() => false))) {
await page.locator("ha-test >> home-assistant-main").evaluate((el) => {
el.dispatchEvent(
new CustomEvent("hass-toggle-menu", {
detail: { open: true },
bubbles: true,
composed: true,
})
);
});
}
await expect(historyLink).toBeVisible({ timeout: SHELL_TIMEOUT });
await historyLink.click();
await expect(page).toHaveURL(/\/#\/history$/, { timeout: SHELL_TIMEOUT });
await expect(
page.locator("ha-panel-history, history-panel").first()
).toBeAttached({ timeout: PANEL_TIMEOUT });
});
test("sidebar renders notification badge", async ({ page }) => {
await goToPanel(page, "/lovelace");
const sidebar = page.locator(
"ha-test >> home-assistant-main >> ha-sidebar"
);
await expect(sidebar).toBeAttached({ timeout: SHELL_TIMEOUT });
const notificationsLink = sidebar.locator("#sidebar-notifications");
await expect(notificationsLink).toBeAttached({ timeout: SHELL_TIMEOUT });
await expect(notificationsLink.locator(".badge").first()).toHaveText("1", {
timeout: SHELL_TIMEOUT,
});
});
test("sidebar marks the active panel as selected", async ({ page }) => {
const sidebar = page.locator(
"ha-test >> home-assistant-main >> ha-sidebar"
);
const lovelaceLink = sidebar.locator("#sidebar-panel-lovelace");
const historyLink = sidebar.locator("#sidebar-panel-history");
await goToPanel(page, "/lovelace");
await expect(lovelaceLink).toHaveClass(/selected/, {
timeout: SHELL_TIMEOUT,
});
await expect(historyLink).not.toHaveClass(/selected/);
await goToPanel(page, "/history");
await expect(historyLink).toHaveClass(/selected/, {
timeout: SHELL_TIMEOUT,
});
await expect(lovelaceLink).not.toHaveClass(/selected/);
});
test("non-admin user does NOT see config panel in sidebar", async ({
page,
}) => {
@@ -158,6 +223,15 @@ test.describe("Panel navigation", () => {
});
});
test("navigates to map panel", async ({ page }) => {
await goToPanel(page, "/map");
await expect(
page.locator("ha-panel-lovelace, hui-root").first()
).toBeAttached({
timeout: PANEL_TIMEOUT,
});
});
test("navigates to history panel", async ({ page }) => {
await goToPanel(page, "/history");
await expect(
@@ -167,14 +241,6 @@ test.describe("Panel navigation", () => {
});
});
test("navigates to developer-tools panel", async ({ page }) => {
// Since 2026.2 developer-tools is part of the config panel
await goToPanel(page, "/config/developer-tools");
await expect(
page.locator("ha-panel-config, developer-tools-main").first()
).toBeAttached({ timeout: PANEL_TIMEOUT });
});
test("navigates to profile panel", async ({ page }) => {
await goToPanel(page, "/profile");
await expect(
@@ -183,6 +249,79 @@ test.describe("Panel navigation", () => {
});
});
// ---------------------------------------------------------------------------
// Tools panel (formerly Developer tools)
// ---------------------------------------------------------------------------
/**
* Every tool sub-page reachable under /config/tools, mapped to the custom
* element tools-router mounts for it (see tools-router.ts). Asserting on the
* specific element proves the route actually rendered its tool, not just the
* shared ha-panel-tools shell.
*/
const TOOLS_SUBPAGES: { route: string; element: string }[] = [
{ route: "yaml", element: "tools-yaml-config" },
{ route: "state", element: "tools-state" },
{ route: "action", element: "tools-action" },
{ route: "template", element: "tools-template" },
{ route: "event", element: "tools-event" },
{ route: "statistics", element: "tools-statistics" },
{ route: "assist", element: "tools-assist" },
{ route: "debug", element: "tools-debug" },
];
test.describe("Tools panel", () => {
test("base path renders the tools panel", async ({ page }) => {
await goToPanel(page, "/config/tools");
await expect(page.locator("ha-panel-tools")).toBeAttached({
timeout: PANEL_TIMEOUT,
});
});
for (const { route, element } of TOOLS_SUBPAGES) {
test(`renders the ${route} sub-page`, async ({ page }) => {
await goToPanel(page, `/config/tools/${route}`);
await expect(page.locator(element)).toBeAttached({
timeout: PANEL_TIMEOUT,
});
});
}
test("service is an alias for the action tool", async ({ page }) => {
await goToPanel(page, "/config/tools/service");
await expect(page.locator("tools-action")).toBeAttached({
timeout: PANEL_TIMEOUT,
});
});
});
// ---------------------------------------------------------------------------
// Tools redirects (old developer-tools URLs)
// ---------------------------------------------------------------------------
test.describe("Tools redirects", () => {
// The panel moved from top-level /developer-tools (pre-2026.2) to
// /config/developer-tools (2026.2), then was renamed to /config/tools
// (2026.8). Both old locations must redirect to the new one, and deep links
// must keep their sub-page. See the updateRoute() redirect in
// src/layouts/home-assistant.ts.
for (const oldBase of ["/developer-tools", "/config/developer-tools"]) {
test(`redirects ${oldBase} to the tools panel`, async ({ page }) => {
await goToPanel(page, oldBase);
await expect(page.locator("ha-panel-tools")).toBeAttached({
timeout: PANEL_TIMEOUT,
});
});
test(`redirects ${oldBase}/state to the state tool`, async ({ page }) => {
await goToPanel(page, `${oldBase}/state`);
await expect(page.locator("tools-state")).toBeAttached({
timeout: PANEL_TIMEOUT,
});
});
}
});
// ---------------------------------------------------------------------------
// Lovelace
// ---------------------------------------------------------------------------
-7
View File
@@ -43,11 +43,4 @@ export const e2eTestPanels: Panels = {
config: null,
url_path: "profile",
},
"developer-tools": {
component_name: "developer-tools",
icon: "mdi:hammer",
title: "developer_tools",
config: null,
url_path: "developer-tools",
},
};
+1 -1
View File
@@ -54,7 +54,7 @@ export class HaTest extends HomeAssistantAppEl {
: scenarios.default;
const initial: Partial<MockHomeAssistant> = {
// Use the full panel map (history + config + developer-tools enabled)
// Use the full panel map (history + config enabled)
panels: e2eTestPanels,
panelUrl: (() => {
const path = window.location.pathname;
+5 -1
View File
@@ -89,6 +89,8 @@ export interface EnergyDataOptions {
period?: "5minute" | "hour" | "day";
compare?: boolean;
prefs?: EnergyPreferences;
/** Probability a period is missing (creates gaps); 0 for a dense dataset. */
gapChance?: number;
}
const statisticIdsForPrefs = (prefs: EnergyPreferences): string[] => {
@@ -115,7 +117,7 @@ export const generateEnergyData = (
seed: number,
options: EnergyDataOptions
): EnergyData => {
const { days, period = "hour", compare = false } = options;
const { days, period = "hour", compare = false, gapChance } = options;
const prefs = options.prefs ?? generateEnergyPreferences();
const ids = statisticIdsForPrefs(prefs);
const dayMs = 24 * 60 * 60 * 1000;
@@ -124,6 +126,7 @@ export const generateEnergyData = (
ids,
period,
days,
gapChance,
sumStatistics: true,
});
const statsCompare = compare
@@ -132,6 +135,7 @@ export const generateEnergyData = (
period,
days,
startMs: FIXED_EPOCH_MS - days * dayMs,
gapChance,
sumStatistics: true,
})
: ({} as EnergyData["statsCompare"]);
@@ -25,8 +25,8 @@ exports[`generateEnergyGasGraphData > large 5-minute payload digest is stable (c
"unit",
"yAxisFractionDigits",
],
"numberCount": 392840,
"numberSum": "2.71986228397e+17",
"numberCount": 312488,
"numberSum": "2.26308627397e+17",
"type": "object",
}
`;
@@ -52,15 +52,6 @@ exports[`generateEnergyGasGraphData > matches snapshot for a single gas source (
"color": "#1b7ea07F",
"cursor": "default",
"data": [
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704069000000,
0,
],
},
{
"itemStyle": {
"borderRadius": [
@@ -3385,438 +3376,6 @@ exports[`generateEnergyGasGraphData > matches snapshot with compare data 1`] = `
0.287,
1704063600000,
],
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704069000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704072600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704076200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704079800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704083400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704087000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704090600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704094200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704097800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704101400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704105000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704108600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704112200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704115800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704119400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704123000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704126600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704130200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704133800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704137400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704141000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704144600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704148200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704151800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704155400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704159000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704162600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704166200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704169800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704173400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704177000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704180600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704184200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704187800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704191400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704195000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704198600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704202200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704205800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704209400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704213000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704216600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704220200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704223800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704227400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704231000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704234600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704238200000,
0,
],
},
],
"id": "compare-sensor.gas_consumption_0",
"itemStyle": {
@@ -4551,438 +4110,6 @@ exports[`generateEnergyGasGraphData > matches snapshot with compare data 1`] = `
1704063600000,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704069000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704072600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704076200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704079800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704083400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704087000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704090600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704094200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704097800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704101400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704105000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704108600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704112200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704115800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704119400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704123000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704126600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704130200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704133800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704137400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704141000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704144600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704148200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704151800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704155400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704159000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704162600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704166200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704169800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704173400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704177000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704180600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704184200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704187800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704191400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704195000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704198600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704202200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704205800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704209400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704213000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704216600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704220200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704223800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704227400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704231000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704234600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704238200000,
0,
],
},
],
"id": "compare-sensor.gas_consumption_1",
"itemStyle": {
@@ -11,8 +11,8 @@ exports[`generateEnergySolarGraphData > large 5-minute payload digest is stable
"total",
"yAxisFractionDigits",
],
"numberCount": 285622,
"numberSum": "1.81353699874e+17",
"numberCount": 232066,
"numberSum": "1.50908786888e+17",
"type": "object",
}
`;
@@ -1008,15 +1008,6 @@ exports[`generateEnergySolarGraphData > matches snapshot for the daily period 1`
1705190400000,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1705276800000,
0,
],
},
{
"itemStyle": {
"borderRadius": [
@@ -1272,15 +1263,6 @@ exports[`generateEnergySolarGraphData > matches snapshot for the daily period 1`
1706745600000,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1706832000000,
0,
],
},
{
"itemStyle": {
"borderRadius": [
@@ -1657,438 +1639,6 @@ exports[`generateEnergySolarGraphData > matches snapshot for two solar sources w
0.287,
1704063600000,
],
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704069000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704072600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704076200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704079800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704083400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704087000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704090600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704094200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704097800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704101400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704105000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704108600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704112200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704115800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704119400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704123000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704126600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704130200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704133800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704137400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704141000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704144600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704148200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704151800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704155400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704159000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704162600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704166200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704169800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704173400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704177000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704180600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704184200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704187800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704191400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704195000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704198600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704202200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704205800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704209400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704213000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704216600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704220200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704223800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704227400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704231000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704234600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704238200000,
0,
],
},
],
"id": "compare-sensor.solar_production",
"itemStyle": {
@@ -2823,438 +2373,6 @@ exports[`generateEnergySolarGraphData > matches snapshot for two solar sources w
1704063600000,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704069000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704072600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704076200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704079800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704083400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704087000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704090600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704094200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704097800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704101400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704105000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704108600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704112200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704115800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704119400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704123000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704126600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704130200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704133800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704137400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704141000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704144600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704148200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704151800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704155400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704159000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704162600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704166200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704169800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704173400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704177000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704180600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704184200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704187800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704191400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704195000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704198600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704202200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704205800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704209400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704213000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704216600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704220200000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704223800000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704227400000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704231000000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704234600000,
0,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704238200000,
0,
],
},
],
"id": "compare-sensor.solar_production_1",
"itemStyle": {
@@ -6476,15 +5594,6 @@ exports[`generateEnergySolarGraphData > matches snapshot with hourly forecast da
1704085200000,
],
},
{
"itemStyle": {
"borderWidth": 0,
},
"value": [
1704090600000,
0,
],
},
{
"itemStyle": {
"borderRadius": [
@@ -5,9 +5,7 @@ import {
computeStatMidpoint,
fillDataGapsAndRoundCaps,
fillLineGaps,
generateFillBuckets,
getCompareTransform,
getPeriodMidpointOffset,
getSuggestedMax,
splitUntrackedConsumption,
} from "../../../../../../src/panels/lovelace/cards/energy/common/energy-chart-options";
@@ -500,307 +498,6 @@ describe("fillDataGapsAndRoundCaps", () => {
assert.equal(datasets[0].data!.length, 0);
});
it("does not fill trailing buckets without an explicit grid", () => {
// Legacy behavior pin: buckets past a dataset's last real point are
// only appended when extraBuckets is passed.
const datasets: BarSeriesOption[] = [
{
type: "bar",
stack: "a",
data: [[1000, 10]],
},
{
type: "bar",
stack: "a",
data: [
[1000, 100],
[2000, 200],
],
},
];
fillDataGapsAndRoundCaps(datasets);
assert.equal(datasets[0].data!.length, 1);
assert.equal(datasets[1].data!.length, 2);
});
it("appends trailing zero buckets from the explicit grid", () => {
// The single-reading case: one real point in the first bucket must
// be padded with zero buckets across the whole grid, otherwise ECharts
// derives a degenerate bar band width and expands the time axis.
const datasets: BarSeriesOption[] = [
{
type: "bar",
stack: "a",
data: [[1000, 10]],
},
];
fillDataGapsAndRoundCaps(datasets, true, [1000, 2000, 3000, 4000]);
assert.equal(datasets[0].data!.length, 4);
assert.equal(getBarItem(datasets[0], 0).value[1], 10);
for (const index of [1, 2, 3]) {
const item = getBarItem(datasets[0], index);
assert.equal(item.value[0], 1000 * (index + 1));
assert.equal(item.value[1], 0);
assert.equal(item.itemStyle.borderWidth, 0);
}
});
it("fills leading and middle buckets from the explicit grid", () => {
const datasets: BarSeriesOption[] = [
{
type: "bar",
stack: "a",
data: [[3000, 30]],
},
];
fillDataGapsAndRoundCaps(datasets, true, [1000, 2000, 3000, 4000]);
assert.equal(datasets[0].data!.length, 4);
assert.deepEqual(
datasets[0].data!.map((item) => getX(item)),
[1000, 2000, 3000, 4000]
);
assert.deepEqual(
datasets[0].data!.map((item) => getY(item)),
[0, 0, 30, 0]
);
});
it("keeps originally-empty datasets empty when a grid is passed", () => {
// Compare placeholder datasets must stay empty so no-data detection
// keeps working.
const datasets: BarSeriesOption[] = [
{
type: "bar",
stack: "a",
data: [],
},
{
type: "bar",
stack: "a",
data: [[1000, 10]],
},
];
fillDataGapsAndRoundCaps(datasets, true, [1000, 2000]);
assert.equal(datasets[0].data!.length, 0);
assert.equal(datasets[1].data!.length, 2);
});
it("still rounds caps on the real bar when grid buckets are added", () => {
const datasets: BarSeriesOption[] = [
{
type: "bar",
stack: "a",
data: [[2000, 10]],
},
];
fillDataGapsAndRoundCaps(datasets, true, [1000, 2000, 3000]);
const realItem = getBarItem(datasets[0], 1);
assert.equal(realItem.value[1], 10);
assert.deepEqual(realItem.itemStyle.borderRadius, [4, 4, 0, 0]);
// Zero fills get no border at all
assert.equal(getBarItem(datasets[0], 0).itemStyle.borderWidth, 0);
assert.equal(getBarItem(datasets[0], 2).itemStyle.borderWidth, 0);
});
});
describe("getPeriodMidpointOffset", () => {
const HOUR = 60 * 60 * 1000;
it("returns half the nominal period when no gap was measured", () => {
assert.equal(getPeriodMidpointOffset("hour"), HOUR / 2);
assert.equal(getPeriodMidpointOffset("5minute"), 2.5 * 60 * 1000);
});
it("returns 0 for daily and longer periods", () => {
assert.equal(getPeriodMidpointOffset("day"), 0);
assert.equal(getPeriodMidpointOffset("week"), 0);
assert.equal(getPeriodMidpointOffset("month"), 0);
// Even with a measured gap
assert.equal(getPeriodMidpointOffset("day", 24 * HOUR), 0);
});
it("uses half the measured gap for finer-grained data", () => {
// e.g. 5-minute data shown with an hourly period
assert.equal(
getPeriodMidpointOffset("hour", 5 * 60 * 1000),
2.5 * 60 * 1000
);
});
it("clamps the measured gap to the nominal period for sparse data", () => {
// e.g. two readings 12h apart in an hourly view
assert.equal(getPeriodMidpointOffset("hour", 12 * HOUR), HOUR / 2);
});
});
describe("generateFillBuckets", () => {
const HOUR = 60 * 60 * 1000;
// Tests run in TZ=Etc/UTC, so local time equals UTC here.
const start = new Date("2024-03-15T00:00:00.000Z");
const end = new Date("2024-03-16T00:00:00.000Z");
const barsAt = (xs: number[], id = "main"): BarSeriesOption => ({
type: "bar",
id,
data: xs.map((x) => [x, 1, x - HOUR / 2]),
});
it("expands a single hourly bucket to the full day grid", () => {
// Real bucket: 09:00-10:00 centered at 09:30
const anchor = start.getTime() + 9.5 * HOUR;
const buckets = generateFillBuckets([barsAt([anchor])], start, end, "hour");
assert.equal(buckets.length, 24);
const sorted = [...buckets].sort((a, b) => a - b);
assert.equal(sorted[0], start.getTime() + 0.5 * HOUR);
assert.equal(sorted[23], start.getTime() + 23.5 * HOUR);
for (let i = 1; i < sorted.length; i++) {
assert.equal(sorted[i] - sorted[i - 1], HOUR);
}
assert.include(buckets, anchor);
});
it("keeps the grid anchored on data not aligned to the range start", () => {
// Half-hour timezone simulation: recorder buckets sit at :30 local, so
// midpoints are on the whole hour. The grid must follow the data, not
// the local-midnight range start.
const anchor = start.getTime() + 10 * HOUR; // 09:30-10:30 bucket
const buckets = generateFillBuckets([barsAt([anchor])], start, end, "hour");
const sorted = [...buckets].sort((a, b) => a - b);
assert.equal(sorted[0], start.getTime());
assert.equal(sorted[sorted.length - 1], start.getTime() + 23 * HOUR);
assert.isTrue(sorted.every((ts) => (ts - anchor) % HOUR === 0));
});
it("generates 5minute buckets", () => {
const fiveMin = 5 * 60 * 1000;
const anchor = start.getTime() + fiveMin / 2;
const shortEnd = new Date(start.getTime() + HOUR);
const buckets = generateFillBuckets(
[barsAt([anchor])],
start,
shortEnd,
"5minute"
);
assert.equal(buckets.length, 12);
const sorted = [...buckets].sort((a, b) => a - b);
for (let i = 1; i < sorted.length; i++) {
assert.equal(sorted[i] - sorted[i - 1], fiveMin);
}
});
it("generates day buckets at period starts", () => {
const weekEnd = new Date("2024-03-22T00:00:00.000Z");
const anchor = start.getTime() + 3 * 24 * HOUR; // day 4 of the range
const buckets = generateFillBuckets(
[barsAt([anchor])],
start,
weekEnd,
"day"
);
assert.equal(buckets.length, 7);
const sorted = [...buckets].sort((a, b) => a - b);
assert.equal(sorted[0], start.getTime());
assert.equal(sorted[6], start.getTime() + 6 * 24 * HOUR);
});
it("generates month buckets with variable month lengths", () => {
const yearStart = new Date("2024-01-01T00:00:00.000Z");
const yearEnd = new Date("2025-01-01T00:00:00.000Z");
const anchor = Date.UTC(2024, 4, 1); // May 1st
const buckets = generateFillBuckets(
[barsAt([anchor])],
yearStart,
yearEnd,
"month"
);
assert.equal(buckets.length, 12);
const sorted = [...buckets].sort((a, b) => a - b);
for (let month = 0; month < 12; month++) {
assert.equal(sorted[month], Date.UTC(2024, month, 1));
}
});
it("ignores compare series and placeholders when picking the anchor", () => {
const compareAnchor = start.getTime() + 0.25 * HOUR; // off-grid transform
const mainAnchor = start.getTime() + 9.5 * HOUR;
const buckets = generateFillBuckets(
[
{ type: "bar", id: "compare-placeholder", data: [] },
barsAt([compareAnchor], "compare-sensor.water"),
barsAt([mainAnchor], "sensor.water"),
],
start,
end,
"hour"
);
assert.include(buckets, mainAnchor);
assert.isTrue(
buckets.every((ts) => (ts - mainAnchor) % HOUR === 0),
"grid must be anchored on the main series"
);
});
it("skips empty main series and anchors on the next one with data", () => {
const anchor = start.getTime() + 9.5 * HOUR;
const buckets = generateFillBuckets(
[barsAt([], "sensor.empty"), barsAt([anchor], "sensor.water")],
start,
end,
"hour"
);
assert.equal(buckets.length, 24);
assert.include(buckets, anchor);
});
it("returns an empty grid when there is no data to anchor on", () => {
assert.deepEqual(generateFillBuckets([], start, end, "hour"), []);
assert.deepEqual(
generateFillBuckets(
[barsAt([], "sensor.empty"), barsAt([1000], "compare-sensor.water")],
start,
end,
"hour"
),
[]
);
});
it("reads the anchor from object-format data items", () => {
const anchor = start.getTime() + 9.5 * HOUR;
const buckets = generateFillBuckets(
[
{
type: "bar",
id: "main",
data: [{ value: [anchor, 1] }],
},
],
start,
end,
"hour"
);
assert.equal(buckets.length, 24);
assert.include(buckets, anchor);
});
});
describe("getCompareTransform", () => {
@@ -4,7 +4,6 @@
* optimization pass — see test/benchmarks/README.md.
*/
import { describe, expect, it } from "vitest";
import type { BarSeriesOption } from "echarts/charts";
import { generateEnergyGasGraphData } from "../../../../../src/panels/lovelace/cards/energy/energy-gas-graph-data";
import type { EnergyPreferences } from "../../../../../src/data/energy";
import type { HomeAssistant } from "../../../../../src/types";
@@ -210,156 +209,4 @@ describe("generateEnergyGasGraphData", () => {
)
).toMatchSnapshot();
});
// Regression tests for #52938: sparse statistics (e.g. a meter that reports
// once per day) must be zero-filled across the whole range, otherwise
// ECharts derives the bar band width from the data gaps — a lone bucket
// makes it expand the time axis by ±40% of its span and draw an oversized
// bar.
describe("sparse data zero-fill", () => {
const HOUR = 60 * 60 * 1000;
const keepBuckets = (
energyData: ReturnType<typeof generateEnergyData>,
hourOffsets: number[]
) => {
const startMs = energyData.start.getTime();
const keep = new Set(hourOffsets.map((h) => startMs + h * HOUR));
return {
...energyData,
stats: Object.fromEntries(
Object.entries(energyData.stats).map(([id, rows]) => [
id,
rows.filter((row) => keep.has(row.start)),
])
),
};
};
const getX = (item: any): number => Number(item?.value?.[0] ?? item?.[0]);
const getY = (item: any): number => Number(item?.value?.[1] ?? item?.[1]);
it("fills the full day grid around a single mid-day bucket", () => {
const energyData = keepBuckets(
generateEnergyData(8, {
days: 1,
period: "hour",
prefs: gasOnlyPrefs(1),
}),
[10]
);
const result = generateEnergyGasGraphData({
hass: makeHass(),
energyData,
computedStyles,
now,
});
const main = result.chartData.find(
(dataset) => dataset.id === "sensor.gas_consumption_0"
)!;
assertDenseGrid(main.data!, 24, HOUR);
const nonZero = main.data!.filter((item) => getY(item) !== 0);
expect(nonZero).toHaveLength(1);
// The real bar stays centered on its bucket midpoint.
expect(getX(nonZero[0])).toBe(energyData.start.getTime() + 10.5 * HOUR);
// The compare placeholder stays empty (no-data detection).
const placeholder = result.chartData.find((dataset) =>
String(dataset.id).startsWith("compare-")
)!;
expect(placeholder.data).toHaveLength(0);
});
it("fills the gaps between sparse readings", () => {
const energyData = keepBuckets(
generateEnergyData(9, {
days: 1,
period: "hour",
prefs: gasOnlyPrefs(1),
}),
[2, 14]
);
const result = generateEnergyGasGraphData({
hass: makeHass(),
energyData,
computedStyles,
now,
});
const main = result.chartData.find(
(dataset) => dataset.id === "sensor.gas_consumption_0"
)!;
assertDenseGrid(main.data!, 24, HOUR);
expect(main.data!.filter((item) => getY(item) !== 0)).toHaveLength(2);
});
it("keeps datasets empty when there is no data at all", () => {
const energyData = keepBuckets(
generateEnergyData(10, {
days: 1,
period: "hour",
prefs: gasOnlyPrefs(1),
}),
[]
);
const result = generateEnergyGasGraphData({
hass: makeHass(),
energyData,
computedStyles,
now,
});
for (const dataset of result.chartData) {
expect(dataset.data).toHaveLength(0);
}
});
it("propagates the grid to compare datasets", () => {
const dayMs = 24 * HOUR;
const base = generateEnergyData(11, {
days: 1,
period: "hour",
compare: true,
prefs: gasOnlyPrefs(1),
});
const energyData = {
...keepBuckets(base, [10]),
// The fixture doesn't set the compare range; provide it so compare
// rows are day-shifted onto the main axis like in the real dashboard.
startCompare: new Date(base.start.getTime() - dayMs),
endCompare: new Date(base.start.getTime()),
};
const result = generateEnergyGasGraphData({
hass: makeHass(),
energyData,
computedStyles,
now,
});
const compare = result.chartData.find(
(dataset) => dataset.id === "compare-sensor.gas_consumption_0"
)!;
// Compare data is dense here, but it must be aligned to the same
// 24-bucket grid as the zero-filled main series.
assertDenseGrid(compare.data!, 24, HOUR);
const main = result.chartData.find(
(dataset) => dataset.id === "sensor.gas_consumption_0"
)!;
assertDenseGrid(main.data!, 24, HOUR);
});
function assertDenseGrid(
data: NonNullable<BarSeriesOption["data"]>,
length: number,
gap: number
) {
expect(data).toHaveLength(length);
const xs = data.map((item) => getX(item));
expect(new Set(xs).size).toBe(length);
const sorted = [...xs].sort((a, b) => a - b);
for (let i = 1; i < sorted.length; i++) {
expect(sorted[i] - sorted[i - 1]).toBe(gap);
}
}
});
});
@@ -164,26 +164,27 @@ describe("generateEnergyDevicesDetailGraphData", () => {
).toMatchSnapshot();
});
// Regression test for #52937/#52938: at the start of the day only the first
// hour has data. The untracked bars must center on the same period midpoint
// as the device bars (one stack, not two), and the whole day must be
// zero-filled so ECharts keeps the configured axis range instead of
// expanding it around the lone bucket.
it("keeps a lone first-of-day bucket on the shared zero-filled grid", () => {
const HOUR = 60 * 60 * 1000;
const full = generateEnergyData(12, {
// Regression test for #52937: at the start of the day only the first hour
// has data. The untracked/over-reported bars must center on the same period
// midpoint as the device bars so they stack as one bar instead of splitting
// into a second stack at the period start.
it("stacks untracked bars on the device bars for a lone first-of-day bucket", () => {
// Full-day range (so getSuggestedPeriod stays "hour") but keep only the
// first hourly bucket in every stat. gapChance: 0 makes the bucket dense.
const full = generateEnergyData(1, {
days: 1,
period: "hour",
gapChance: 0,
prefs: buildPrefs(false),
});
const firstStart = full.start.getTime();
const energyData = {
...full,
stats: Object.fromEntries(
Object.entries(full.stats).map(([id, rows]) => [
id,
rows.filter((row) => row.start === firstStart),
])
Object.entries(full.stats).map(
([id, values]) =>
[id, values.filter((s) => s.start === firstStart)] as const
)
),
};
@@ -192,29 +193,26 @@ describe("generateEnergyDevicesDetailGraphData", () => {
energyData,
});
const nonZeroXs = new Set<number>();
// Collect the display x of every bar across all series.
const xs = new Set<number>();
let nonEmptySeries = 0;
for (const series of result.chartData) {
if (!series.data?.length) {
continue;
const points = series.data ?? [];
if (points.length) {
nonEmptySeries++;
}
// Every non-empty series covers the full day grid...
const xs = series.data.map((item: any) =>
Number(item?.value?.[0] ?? item?.[0])
);
assert.equal(xs.length, 24);
const sorted = [...xs].sort((a, b) => a - b);
for (let i = 1; i < sorted.length; i++) {
assert.equal(sorted[i] - sorted[i - 1], HOUR);
}
for (const [index, item] of (series.data as any[]).entries()) {
const y = Number(item?.value?.[1] ?? item?.[1]);
if (y !== 0) {
nonZeroXs.add(xs[index]);
for (const point of points as any[]) {
const x = Array.isArray(point) ? point[0] : point?.value?.[0];
if (x != null) {
xs.add(Number(x));
}
}
}
// ...and all real values stack on the single bucket midpoint.
assert.deepEqual([...nonZeroXs], [firstStart + HOUR / 2]);
// Device bars + at least one untracked series are present...
assert.isAtLeast(nonEmptySeries, 2);
// ...and they all share a single x, so they render as one full stack.
assert.equal(xs.size, 1);
});
// The seeded fixtures above all happen to produce fully-negative untracked
@@ -209,57 +209,4 @@ describe("generateEnergySolarGraphData", () => {
)
).toMatchSnapshot();
});
// Regression test for #52938: a lone statistics bucket must be zero-filled
// across the day so ECharts keeps the configured axis range, while forecast
// line series stay untouched by the bar-bucket fill.
it("zero-fills bars around a single bucket without touching forecast lines", () => {
const HOUR = 60 * 60 * 1000;
const base = generateEnergyData(7, {
days: 1,
period: "hour",
prefs: solarPrefs({ sources: 1, forecast: true }),
});
const startMs = base.start.getTime();
const energyData = {
...base,
stats: Object.fromEntries(
Object.entries(base.stats).map(([id, rows]) => [
id,
rows.filter((row) => row.start === startMs + 10 * HOUR),
])
),
};
const forecasts = buildForecasts(24, HOUR, ["entry_0"]);
const result = generateEnergySolarGraphData({
hass,
energyData,
forecasts,
computedStyles,
now,
});
const bars = result.chartData.find(
(d) => d.id === "sensor.solar_production"
)!;
const xs = bars.data!.map((item: any) =>
Number(item?.value?.[0] ?? item?.[0])
);
expect(xs).toHaveLength(24);
const sorted = [...xs].sort((a, b) => a - b);
for (let i = 1; i < sorted.length; i++) {
expect(sorted[i] - sorted[i - 1]).toBe(HOUR);
}
const forecast = result.chartData.find((d) =>
String(d.id).startsWith("forecast-")
)!;
expect(forecast.data).toHaveLength(24);
// Forecast points are centered with the nominal half-period offset.
for (const [index, item] of (forecast.data as any[]).entries()) {
expect(Number(item?.value?.[0] ?? item?.[0])).toBe(
startMs + index * HOUR + HOUR / 2
);
}
});
});
+55 -55
View File
@@ -4618,22 +4618,22 @@ __metadata:
languageName: node
linkType: hard
"@rsdoctor/client@npm:1.5.16":
version: 1.5.16
resolution: "@rsdoctor/client@npm:1.5.16"
checksum: 10/dcda4e8034a296090b073102423050764e636f9801f2e5a5904e1f2744b1fe26d5f8202aed7a785c9fc809044e1aef5c4b6a16b63974caec79d1b5996ec35d34
"@rsdoctor/client@npm:1.5.17":
version: 1.5.17
resolution: "@rsdoctor/client@npm:1.5.17"
checksum: 10/0eb788455390a1b41aa31d982d93ceab3dd30671776e40e8a4ea3256b4713f6441066e079ff9a14413825e21d547b9b7d4ba52059f8995644e26724ff07bbf56
languageName: node
linkType: hard
"@rsdoctor/core@npm:1.5.16":
version: 1.5.16
resolution: "@rsdoctor/core@npm:1.5.16"
"@rsdoctor/core@npm:1.5.17":
version: 1.5.17
resolution: "@rsdoctor/core@npm:1.5.17"
dependencies:
"@rsbuild/plugin-check-syntax": "npm:^1.6.1"
"@rsdoctor/graph": "npm:1.5.16"
"@rsdoctor/sdk": "npm:1.5.16"
"@rsdoctor/types": "npm:1.5.16"
"@rsdoctor/utils": "npm:1.5.16"
"@rsdoctor/graph": "npm:1.5.17"
"@rsdoctor/sdk": "npm:1.5.17"
"@rsdoctor/types": "npm:1.5.17"
"@rsdoctor/utils": "npm:1.5.17"
"@rspack/resolver": "npm:^0.2.8"
browserslist-load-config: "npm:^1.0.2"
es-toolkit: "npm:^1.47.0"
@@ -4641,60 +4641,60 @@ __metadata:
fs-extra: "npm:^11.1.1"
semver: "npm:^7.7.4"
source-map: "npm:^0.7.6"
checksum: 10/be7b03b5a5a8a9be47f94159469c35488f98046c99e2ccd7daed325c3dd2a8b21c654c12ac6d40c2775546efade373f429f630e8905cc17a5c9151978a0caaf9
checksum: 10/a797d5243d1d3f758d8b38cea1a3195345525c3158c4061f5e78d875fe2001198c8967587153059eb7c5ff764f27b4685ce13fd55a27dccdcfe7cd8061fb30c9
languageName: node
linkType: hard
"@rsdoctor/graph@npm:1.5.16":
version: 1.5.16
resolution: "@rsdoctor/graph@npm:1.5.16"
"@rsdoctor/graph@npm:1.5.17":
version: 1.5.17
resolution: "@rsdoctor/graph@npm:1.5.17"
dependencies:
"@rsdoctor/types": "npm:1.5.16"
"@rsdoctor/utils": "npm:1.5.16"
"@rsdoctor/types": "npm:1.5.17"
"@rsdoctor/utils": "npm:1.5.17"
es-toolkit: "npm:^1.47.0"
path-browserify: "npm:1.0.1"
source-map: "npm:^0.7.6"
checksum: 10/949e3a2cc48ccbb2d554becb2270c4df4b4fc8a6e10bd55bf9dc4d5f9a5fb2823c3e11a30dce890beaaa1b0ab0039bba1554ad0e7ddc5e2ea47641222d454633
checksum: 10/e58ed532ea8cc743e45dd66b678e1da3d48939fe711ebfade47834ffc581be6089d611d18c97c3e12010e2c609049cb325d07021a5dc51ef651314f8fe9f5741
languageName: node
linkType: hard
"@rsdoctor/rspack-plugin@npm:1.5.16":
version: 1.5.16
resolution: "@rsdoctor/rspack-plugin@npm:1.5.16"
"@rsdoctor/rspack-plugin@npm:1.5.17":
version: 1.5.17
resolution: "@rsdoctor/rspack-plugin@npm:1.5.17"
dependencies:
"@rsdoctor/core": "npm:1.5.16"
"@rsdoctor/graph": "npm:1.5.16"
"@rsdoctor/sdk": "npm:1.5.16"
"@rsdoctor/types": "npm:1.5.16"
"@rsdoctor/utils": "npm:1.5.16"
"@rsdoctor/core": "npm:1.5.17"
"@rsdoctor/graph": "npm:1.5.17"
"@rsdoctor/sdk": "npm:1.5.17"
"@rsdoctor/types": "npm:1.5.17"
"@rsdoctor/utils": "npm:1.5.17"
peerDependencies:
"@rspack/core": "*"
peerDependenciesMeta:
"@rspack/core":
optional: true
checksum: 10/2bebf2b8dfc5ffde77b46b45fedc7d5d9b96f4fe3e5e1b3762bff5de6f278d9288fe6c361fa1df5bb17926a45ae3c6038e165d4f1f5e867724df0a530590b36a
checksum: 10/336bd813010a7c164770033ae5a30644bf165ce0dff250b9160c7c003401c214bfd96e528c5941c09834bb21949a4815c46ecae0ca4d9200b2b946b9c3164f8a
languageName: node
linkType: hard
"@rsdoctor/sdk@npm:1.5.16":
version: 1.5.16
resolution: "@rsdoctor/sdk@npm:1.5.16"
"@rsdoctor/sdk@npm:1.5.17":
version: 1.5.17
resolution: "@rsdoctor/sdk@npm:1.5.17"
dependencies:
"@rsdoctor/client": "npm:1.5.16"
"@rsdoctor/graph": "npm:1.5.16"
"@rsdoctor/types": "npm:1.5.16"
"@rsdoctor/utils": "npm:1.5.16"
"@rsdoctor/client": "npm:1.5.17"
"@rsdoctor/graph": "npm:1.5.17"
"@rsdoctor/types": "npm:1.5.17"
"@rsdoctor/utils": "npm:1.5.17"
launch-editor: "npm:^2.13.2"
safer-buffer: "npm:2.1.2"
socket.io: "npm:4.8.1"
tapable: "npm:2.3.3"
checksum: 10/8a845468e13c66b93f9784c7887f7040b1df24f43e9304b50c3a7258c6b172c2bc3ca5f6e5e9d15801d1634e3e48f6bc78115a7a0242890548d111ae512caf7d
checksum: 10/d8a146a43726d61a9d7d2cfca7e2cd48c42a7ba28c9d280353f986f1647c0a33f05ec68a45af4f22104df8a8dae7dc14d621e56a15f11c3967d78c21216be234
languageName: node
linkType: hard
"@rsdoctor/types@npm:1.5.16":
version: 1.5.16
resolution: "@rsdoctor/types@npm:1.5.16"
"@rsdoctor/types@npm:1.5.17":
version: 1.5.17
resolution: "@rsdoctor/types@npm:1.5.17"
dependencies:
"@types/connect": "npm:3.4.38"
"@types/estree": "npm:1.0.5"
@@ -4708,16 +4708,16 @@ __metadata:
optional: true
webpack:
optional: true
checksum: 10/f470a7047474669bd466c9cee15b5ef3e4b854d4347e3dd4f251f1d1f92bd9b7b5e6863349f5d3550485c3484497a1e8be71cefc56e883c81f8a734960d1fc5e
checksum: 10/4767825ae55498e25d1dfbecc0aebe2685b67701dae004617f4d95a68b85f19f29afe75f72b8efeab4fe49185bb9a3c85dcfce8f99852c49d4ee37a4f6b7d888
languageName: node
linkType: hard
"@rsdoctor/utils@npm:1.5.16":
version: 1.5.16
resolution: "@rsdoctor/utils@npm:1.5.16"
"@rsdoctor/utils@npm:1.5.17":
version: 1.5.17
resolution: "@rsdoctor/utils@npm:1.5.17"
dependencies:
"@babel/code-frame": "npm:7.26.2"
"@rsdoctor/types": "npm:1.5.16"
"@rsdoctor/types": "npm:1.5.17"
"@types/estree": "npm:1.0.5"
acorn: "npm:^8.10.0"
acorn-import-attributes: "npm:^1.9.5"
@@ -4731,7 +4731,7 @@ __metadata:
picocolors: "npm:^1.1.1"
rslog: "npm:^2.1.2"
strip-ansi: "npm:^6.0.1"
checksum: 10/d73062cc01f4e2def276d6515f2810f54bfd7f819f1d3a00dd884621f9c008eda4b31ae6e6d96efaec81d1bbad98f0f49cb77e14a028d5f53c97aca84b6b07d4
checksum: 10/7c9b4a3824de61f6254df50f80c5efe53df662f30cb07047afa956a9bc3917dc71bf0aa73c8edde7f73f9577a9cb051e1a81caaffa118db414a4b655223fe92a
languageName: node
linkType: hard
@@ -9771,7 +9771,7 @@ __metadata:
"@octokit/rest": "npm:22.0.1"
"@playwright/test": "npm:1.61.1"
"@replit/codemirror-indentation-markers": "npm:6.5.3"
"@rsdoctor/rspack-plugin": "npm:1.5.16"
"@rsdoctor/rspack-plugin": "npm:1.5.17"
"@rspack/core": "npm:2.1.1"
"@rspack/dev-server": "npm:2.1.0"
"@swc/helpers": "npm:0.5.23"
@@ -9837,7 +9837,7 @@ __metadata:
home-assistant-js-websocket: "npm:9.6.0"
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"
idb-keyval: "npm:6.2.5"
idb-keyval: "npm:6.2.6"
intl-messageformat: "npm:11.2.9"
js-yaml: "npm:5.2.0"
jsdom: "npm:29.1.1"
@@ -9860,7 +9860,7 @@ __metadata:
node-vibrant: "npm:4.0.4"
object-hash: "npm:3.0.0"
pinst: "npm:3.0.0"
prettier: "npm:3.9.1"
prettier: "npm:3.9.3"
punycode: "npm:2.3.1"
qr-scanner: "npm:1.4.2"
qrcode: "npm:1.5.4"
@@ -10056,10 +10056,10 @@ __metadata:
languageName: node
linkType: hard
"idb-keyval@npm:6.2.5":
version: 6.2.5
resolution: "idb-keyval@npm:6.2.5"
checksum: 10/ac645882b3258ff07347d085baab91b871bac7be4f46ff8e20a7c036c2df35d3f695a30050009f27237b99045203568f2a842a35295a48f9b815959ee51a347e
"idb-keyval@npm:6.2.6":
version: 6.2.6
resolution: "idb-keyval@npm:6.2.6"
checksum: 10/8d0f8b9bd5eead685731a900510095dbc58936968739755bfd1de1c69a710daa5eb2b5cf185d0a7c7e9ce1daf4544fa5f58a2c7a37258a6826dd40f9e2614245
languageName: node
linkType: hard
@@ -12746,12 +12746,12 @@ __metadata:
languageName: node
linkType: hard
"prettier@npm:3.9.1":
version: 3.9.1
resolution: "prettier@npm:3.9.1"
"prettier@npm:3.9.3":
version: 3.9.3
resolution: "prettier@npm:3.9.3"
bin:
prettier: bin/prettier.cjs
checksum: 10/1b4317674aa9e90ff79c347fd19f91bb305df98b3122e7131d6815291707781305c45a13cd982474c2f74ed748f2fd0a9aa094f9856609ed1b6f092de8152058
checksum: 10/2aa4232a7ae2204a6d0758e8083117509f13a499ae49f87ed8e4a9c15967083f400e4e189a64948de60987974d3441f4b5e5110e7a3e56f9b83f4e2904cef376
languageName: node
linkType: hard