mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-16 16:39:48 +00:00
Compare commits
74 Commits
20250526.0
...
ha-icon-pa
Author | SHA1 | Date | |
---|---|---|---|
![]() |
31c56eb8b5 | ||
![]() |
01d2ef13c6 | ||
![]() |
af6911e848 | ||
![]() |
21af10fd28 | ||
![]() |
6d30d15638 | ||
![]() |
d542b52ebd | ||
![]() |
43cc49bb32 | ||
![]() |
b3f0a6328e | ||
![]() |
9d6a7e7e6f | ||
![]() |
78d7da21aa | ||
![]() |
0474a24df6 | ||
![]() |
6e7ac6fdf7 | ||
![]() |
7b9683df89 | ||
![]() |
8523ddfd29 | ||
![]() |
2589e1a49f | ||
![]() |
5ce5f9a189 | ||
![]() |
6dd7217a20 | ||
![]() |
0d02d0d334 | ||
![]() |
fed0dfa091 | ||
![]() |
39de40dec9 | ||
![]() |
e1c42d9985 | ||
![]() |
ad375c9b01 | ||
![]() |
07230e5ef5 | ||
![]() |
52f5af6090 | ||
![]() |
3c07289077 | ||
![]() |
8eb7fe8b0a | ||
![]() |
c8c2966d34 | ||
![]() |
a8768a5d9d | ||
![]() |
02bb7086e7 | ||
![]() |
42d8b2ae19 | ||
![]() |
e08f4a6bba | ||
![]() |
2e6c35d977 | ||
![]() |
17305a818b | ||
![]() |
08389dad04 | ||
![]() |
ab6ace46b5 | ||
![]() |
535dedbbc4 | ||
![]() |
412eb0c647 | ||
![]() |
87c8ebd493 | ||
![]() |
6e49f89126 | ||
![]() |
a099e65a9d | ||
![]() |
11e4a9f056 | ||
![]() |
b617299eee | ||
![]() |
768f27b1b9 | ||
![]() |
5ed816df6d | ||
![]() |
6692ac7517 | ||
![]() |
65499db0cb | ||
![]() |
11a1eabf61 | ||
![]() |
b30fa122ba | ||
![]() |
6730d08b85 | ||
![]() |
67003d6fd1 | ||
![]() |
414d46be65 | ||
![]() |
1485d1a1de | ||
![]() |
fd13e41524 | ||
![]() |
77f7ca0368 | ||
![]() |
7471250a07 | ||
![]() |
8b0a63d791 | ||
![]() |
57ffa814ed | ||
![]() |
16e20456e2 | ||
![]() |
9c0ce41ebb | ||
![]() |
b458a1d7c6 | ||
![]() |
0eaeeb1141 | ||
![]() |
b7e63e697f | ||
![]() |
06db0f4b98 | ||
![]() |
d33636c6fb | ||
![]() |
bbb546159c | ||
![]() |
e8fc36026a | ||
![]() |
38f8c804af | ||
![]() |
7c5bf26240 | ||
![]() |
189067d14b | ||
![]() |
e79e0f77b8 | ||
![]() |
b226e5c697 | ||
![]() |
52ad31601c | ||
![]() |
cba3e4df7f | ||
![]() |
3532cfa974 |
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -108,9 +108,9 @@ body:
|
||||
render: yaml
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Javascript errors shown in your browser console/inspector
|
||||
label: JavaScript errors shown in your browser console/inspector
|
||||
description: >
|
||||
If you come across any Javascript or other error logs, e.g., in your
|
||||
If you come across any JavaScript or other error logs, e.g., in your
|
||||
browser console/inspector please provide them.
|
||||
render: txt
|
||||
- type: textarea
|
||||
|
@@ -1 +1 @@
|
||||
yarn run lint-staged --relative --shell "/bin/bash"
|
||||
yarn run lint-staged --relative
|
||||
|
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.9.2.cjs
|
||||
|
@@ -1,7 +1,30 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
let changeFunction;
|
||||
|
||||
export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("frontend/get_user_data", () => ({
|
||||
value: null,
|
||||
}));
|
||||
hass.mockWS("frontend/set_user_data", ({ key, value }) => {
|
||||
if (key === "sidebar") {
|
||||
changeFunction?.({
|
||||
value: {
|
||||
panelOrder: value.panelOrder || [],
|
||||
hiddenPanels: value.hiddenPanels || [],
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
|
||||
changeFunction = onChange;
|
||||
onChange?.({
|
||||
value: {
|
||||
panelOrder: [],
|
||||
hiddenPanels: [],
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
});
|
||||
};
|
||||
|
46
package.json
46
package.json
@@ -26,15 +26,15 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.27.1",
|
||||
"@babel/runtime": "7.27.6",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.18.6",
|
||||
"@codemirror/commands": "6.8.1",
|
||||
"@codemirror/language": "6.11.0",
|
||||
"@codemirror/language": "6.11.1",
|
||||
"@codemirror/legacy-modes": "6.5.1",
|
||||
"@codemirror/search": "6.5.11",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.36.8",
|
||||
"@codemirror/view": "6.37.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.18.0",
|
||||
"@formatjs/intl-displaynames": "6.8.11",
|
||||
@@ -89,8 +89,8 @@
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.8.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vaadin/combo-box": "24.7.7",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.7",
|
||||
"@vaadin/combo-box": "24.7.8",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.8",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
@@ -111,7 +111,7 @@
|
||||
"fuse.js": "7.1.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"hls.js": "1.6.2",
|
||||
"hls.js": "1.6.5",
|
||||
"home-assistant-js-websocket": "9.5.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "10.7.16",
|
||||
@@ -149,20 +149,20 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.27.1",
|
||||
"@babel/core": "7.27.4",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.4",
|
||||
"@babel/plugin-transform-runtime": "7.27.1",
|
||||
"@babel/plugin-transform-runtime": "7.27.4",
|
||||
"@babel/preset-env": "7.27.2",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.20.1",
|
||||
"@lokalise/node-api": "14.7.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.20.2",
|
||||
"@lokalise/node-api": "14.8.0",
|
||||
"@octokit/auth-oauth-device": "8.0.1",
|
||||
"@octokit/plugin-retry": "8.0.1",
|
||||
"@octokit/rest": "21.1.1",
|
||||
"@rsdoctor/rspack-plugin": "1.1.2",
|
||||
"@rspack/cli": "1.3.11",
|
||||
"@rspack/core": "1.3.11",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@rsdoctor/rspack-plugin": "1.1.3",
|
||||
"@rspack/cli": "1.3.12",
|
||||
"@rspack/core": "1.3.12",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.21",
|
||||
"@types/chromecast-caf-receiver": "6.0.22",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/glob": "8.1.0",
|
||||
@@ -179,12 +179,12 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "3.1.4",
|
||||
"@vitest/coverage-v8": "3.2.2",
|
||||
"babel-loader": "10.0.0",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.0",
|
||||
"eslint": "9.27.0",
|
||||
"eslint": "9.28.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.5",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
@@ -196,7 +196,7 @@
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.0",
|
||||
"glob": "11.0.2",
|
||||
"gulp": "5.0.0",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.0.0",
|
||||
@@ -204,7 +204,7 @@
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "26.1.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "15.5.2",
|
||||
"lint-staged": "16.1.0",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
@@ -218,9 +218,9 @@
|
||||
"terser-webpack-plugin": "5.3.14",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.32.1",
|
||||
"typescript-eslint": "8.33.1",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.1.4",
|
||||
"vitest": "3.2.2",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
@@ -232,9 +232,9 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.0",
|
||||
"@fullcalendar/daygrid": "6.1.17",
|
||||
"globals": "16.1.0",
|
||||
"globals": "16.2.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1"
|
||||
"packageManager": "yarn@4.9.2"
|
||||
}
|
||||
|
4
src/common/entity/valid_service_id.ts
Normal file
4
src/common/entity/valid_service_id.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
const validServiceId = /^(\w+)\.(\w+)$/;
|
||||
|
||||
export const isValidServiceId = (actionId: string) =>
|
||||
validServiceId.test(actionId);
|
@@ -1,9 +1,19 @@
|
||||
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
|
||||
export const slugify = (value: string, delimiter = "_") => {
|
||||
const a =
|
||||
"àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìıİłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·";
|
||||
const b = `aaaaaaaaaacccddeeeeeeeegghiiiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}`;
|
||||
"àáâäæãåāăąабçćčđďдèéêëēėęěеёэфğǵгḧхîïíīįìıİийкłлḿмñńǹňнôöòóœøōõőоṕпŕřрßśšşșсťțтûüùúūǘůűųувẃẍÿýыžźżз·";
|
||||
const b = `aaaaaaaaaaabcccdddeeeeeeeeeeefggghhiiiiiiiiijkllmmnnnnnoooooooooopprrrsssssstttuuuuuuuuuuvwxyyyzzzz${delimiter}`;
|
||||
const p = new RegExp(a.split("").join("|"), "g");
|
||||
const complex_cyrillic = {
|
||||
ж: "zh",
|
||||
х: "kh",
|
||||
ц: "ts",
|
||||
ч: "ch",
|
||||
ш: "sh",
|
||||
щ: "shch",
|
||||
ю: "iu",
|
||||
я: "ia",
|
||||
};
|
||||
|
||||
let slugified;
|
||||
|
||||
@@ -14,6 +24,7 @@ export const slugify = (value: string, delimiter = "_") => {
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
|
||||
.replace(/[а-я]/g, (c) => complex_cyrillic[c] || "") // Replace some cyrillic characters
|
||||
.replace(/(\d),(?=\d)/g, "$1") // Remove Commas between numbers
|
||||
.replace(/[^a-z0-9]+/g, delimiter) // Replace all non-word characters
|
||||
.replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter
|
||||
|
@@ -220,11 +220,11 @@ export class HaChartBase extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
const datasets = ensureArray(this.data);
|
||||
const items = (legend.data ||
|
||||
datasets
|
||||
const items: LegendComponentOption["data"] =
|
||||
legend.data ||
|
||||
((datasets
|
||||
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
|
||||
.map((d) => d.name ?? d.id) ||
|
||||
[]) as string[];
|
||||
.map((d) => d.name ?? d.id) || []) as string[]);
|
||||
|
||||
const isMobile = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
@@ -239,20 +239,32 @@ export class HaChartBase extends LitElement {
|
||||
})}
|
||||
>
|
||||
<ul>
|
||||
${items.map((item: string, index: number) => {
|
||||
${items.map((item, index) => {
|
||||
if (!this.expandLegend && index >= overflowLimit) {
|
||||
return nothing;
|
||||
}
|
||||
const dataset = datasets.find(
|
||||
(d) => d.id === item || d.name === item
|
||||
);
|
||||
const color = dataset?.color as string;
|
||||
const borderColor = dataset?.itemStyle?.borderColor as string;
|
||||
let itemStyle: Record<string, any> = {};
|
||||
let name = "";
|
||||
if (typeof item === "string") {
|
||||
name = item;
|
||||
const dataset = datasets.find(
|
||||
(d) => d.id === item || d.name === item
|
||||
);
|
||||
itemStyle = {
|
||||
color: dataset?.color as string,
|
||||
...(dataset?.itemStyle as { borderColor?: string }),
|
||||
};
|
||||
} else {
|
||||
name = item.name ?? "";
|
||||
itemStyle = item.itemStyle ?? {};
|
||||
}
|
||||
const color = itemStyle?.color as string;
|
||||
const borderColor = itemStyle?.borderColor as string;
|
||||
return html`<li
|
||||
.name=${item}
|
||||
.name=${name}
|
||||
@click=${this._legendClick}
|
||||
class=${classMap({ hidden: this._hiddenDatasets.has(item) })}
|
||||
.title=${item}
|
||||
class=${classMap({ hidden: this._hiddenDatasets.has(name) })}
|
||||
.title=${name}
|
||||
>
|
||||
<div
|
||||
class="bullet"
|
||||
@@ -261,7 +273,7 @@ export class HaChartBase extends LitElement {
|
||||
borderColor: borderColor || color,
|
||||
})}
|
||||
></div>
|
||||
<div class="label">${item}</div>
|
||||
<div class="label">${name}</div>
|
||||
</li>`;
|
||||
})}
|
||||
${items.length > overflowLimit
|
||||
@@ -482,6 +494,13 @@ export class HaChartBase extends LitElement {
|
||||
smooth: false,
|
||||
},
|
||||
bar: { itemStyle: { barBorderWidth: 1.5 } },
|
||||
graph: {
|
||||
label: {
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
textBorderColor: style.getPropertyValue("--primary-background-color"),
|
||||
textBorderWidth: 2,
|
||||
},
|
||||
},
|
||||
categoryAxis: {
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
|
@@ -82,6 +82,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
private _previousYAxisLabelValue = 0;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-chart-base
|
||||
@@ -227,14 +229,20 @@ export class StateHistoryChartLine extends LitElement {
|
||||
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
|
||||
minYAxis = ({ min }) => {
|
||||
const value = min > 0 ? min * 0.95 : min * 1.05;
|
||||
return Math.abs(value) < 1 ? value : Math.floor(value);
|
||||
};
|
||||
}
|
||||
if (typeof maxYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
|
||||
maxYAxis = ({ max }) => {
|
||||
const value = max > 0 ? max * 1.05 : max * 0.95;
|
||||
return Math.abs(value) < 1 ? value : Math.ceil(value);
|
||||
};
|
||||
}
|
||||
this._chartOptions = {
|
||||
xAxis: {
|
||||
@@ -258,32 +266,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
},
|
||||
axisLabel: {
|
||||
margin: 5,
|
||||
formatter: (value: number) => {
|
||||
const formatOptions =
|
||||
value >= 1 || value <= -1
|
||||
? undefined
|
||||
: {
|
||||
// show the first significant digit for tiny values
|
||||
maximumFractionDigits: Math.max(
|
||||
2,
|
||||
-Math.floor(Math.log10(Math.abs(value % 1 || 1)))
|
||||
),
|
||||
};
|
||||
const label = formatNumber(
|
||||
value,
|
||||
this.hass.locale,
|
||||
formatOptions
|
||||
);
|
||||
const width = measureTextWidth(label, 12) + 5;
|
||||
if (width > this._yWidth) {
|
||||
this._yWidth = width;
|
||||
fireEvent(this, "y-width-changed", {
|
||||
value: this._yWidth,
|
||||
chartIndex: this.chartIndex,
|
||||
});
|
||||
}
|
||||
return label;
|
||||
},
|
||||
formatter: this._formatYAxisLabel,
|
||||
},
|
||||
} as YAXisOption,
|
||||
legend: {
|
||||
@@ -745,14 +728,41 @@ export class StateHistoryChartLine extends LitElement {
|
||||
this._visualMap = visualMap.length > 0 ? visualMap : undefined;
|
||||
}
|
||||
|
||||
private _formatYAxisLabel = (value: number) => {
|
||||
const formatOptions =
|
||||
value >= 1 || value <= -1
|
||||
? undefined
|
||||
: {
|
||||
// show the first significant digit for tiny values
|
||||
maximumFractionDigits: Math.max(
|
||||
2,
|
||||
// use the difference to the previous value to determine the number of significant digits #25526
|
||||
-Math.floor(
|
||||
Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1))
|
||||
)
|
||||
),
|
||||
};
|
||||
const label = formatNumber(value, this.hass.locale, formatOptions);
|
||||
const width = measureTextWidth(label, 12) + 5;
|
||||
if (width > this._yWidth) {
|
||||
this._yWidth = width;
|
||||
fireEvent(this, "y-width-changed", {
|
||||
value: this._yWidth,
|
||||
chartIndex: this.chartIndex,
|
||||
});
|
||||
}
|
||||
this._previousYAxisLabelValue = value;
|
||||
return label;
|
||||
};
|
||||
|
||||
private _clampYAxis(value?: number | ((values: any) => number)) {
|
||||
if (this.logarithmicScale) {
|
||||
// log(0) is -Infinity, so we need to set a minimum value
|
||||
if (typeof value === "number") {
|
||||
return Math.max(value, 0.1);
|
||||
return Math.max(value, Number.EPSILON);
|
||||
}
|
||||
if (typeof value === "function") {
|
||||
return (values: any) => Math.max(value(values), 0.1);
|
||||
return (values: any) => Math.max(value(values), Number.EPSILON);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
|
@@ -241,14 +241,20 @@ export class StatisticsChart extends LitElement {
|
||||
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
|
||||
minYAxis = ({ min }) => {
|
||||
const value = min > 0 ? min * 0.95 : min * 1.05;
|
||||
return Math.abs(value) < 1 ? value : Math.floor(value);
|
||||
};
|
||||
}
|
||||
if (typeof maxYAxis === "number") {
|
||||
if (this.fitYData) {
|
||||
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
|
||||
}
|
||||
} else if (this.logarithmicScale) {
|
||||
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
|
||||
maxYAxis = ({ max }) => {
|
||||
const value = max > 0 ? max * 1.05 : max * 0.95;
|
||||
return Math.abs(value) < 1 ? value : Math.ceil(value);
|
||||
};
|
||||
}
|
||||
const endTime = this.endTime ?? new Date();
|
||||
let startTime = this.startTime;
|
||||
@@ -619,10 +625,10 @@ export class StatisticsChart extends LitElement {
|
||||
if (this.logarithmicScale) {
|
||||
// log(0) is -Infinity, so we need to set a minimum value
|
||||
if (typeof value === "number") {
|
||||
return Math.max(value, 0.1);
|
||||
return Math.max(value, Number.EPSILON);
|
||||
}
|
||||
if (typeof value === "function") {
|
||||
return (values: any) => Math.max(value(values), 0.1);
|
||||
return (values: any) => Math.max(value(values), Number.EPSILON);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
|
@@ -740,6 +740,7 @@ export class HaDataTable extends LitElement {
|
||||
}, {});
|
||||
const groupedItems: DataTableRowData[] = [];
|
||||
Object.entries(sorted).forEach(([groupName, rows]) => {
|
||||
const collapsed = collapsedGroups.includes(groupName);
|
||||
groupedItems.push({
|
||||
append: true,
|
||||
selectable: false,
|
||||
@@ -751,9 +752,10 @@ export class HaDataTable extends LitElement {
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronUp}
|
||||
class=${collapsedGroups.includes(groupName)
|
||||
? "collapsed"
|
||||
: ""}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.data-table.${collapsed ? "expand" : "collapse"}`
|
||||
)}
|
||||
class=${collapsed ? "collapsed" : ""}
|
||||
>
|
||||
</ha-icon-button>
|
||||
${groupName === UNDEFINED_GROUP_KEY
|
||||
|
@@ -23,7 +23,10 @@ import type { HomeAssistant } from "../../types";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import type {
|
||||
PickerComboBoxItem,
|
||||
PickerComboBoxSearchFn,
|
||||
} from "../ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
@@ -51,6 +54,9 @@ export class HaEntityPicker extends LitElement {
|
||||
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
||||
public allowCustomEntity;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-entity-id" })
|
||||
public showEntityId = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
@@ -166,11 +172,15 @@ export class HaEntityPicker extends LitElement {
|
||||
`;
|
||||
};
|
||||
|
||||
private get _showEntityId() {
|
||||
return this.showEntityId || this.hass.userData?.showEntityIdPicker;
|
||||
}
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = (
|
||||
item,
|
||||
{ index }
|
||||
) => {
|
||||
const showEntityId = this.hass.userData?.showEntityIdPicker;
|
||||
const showEntityId = this._showEntityId;
|
||||
|
||||
return html`
|
||||
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
||||
@@ -390,6 +400,7 @@ export class HaEntityPicker extends LitElement {
|
||||
.autofocus=${this.autofocus}
|
||||
.allowCustomValue=${this.allowCustomEntity}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.searchLabel=${this.searchLabel}
|
||||
.notFoundLabel=${notFoundLabel}
|
||||
.placeholder=${placeholder}
|
||||
@@ -398,6 +409,7 @@ export class HaEntityPicker extends LitElement {
|
||||
.getItems=${this._getItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
.searchFn=${this._searchFn}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
@@ -405,6 +417,23 @@ export class HaEntityPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _searchFn: PickerComboBoxSearchFn<EntityComboBoxItem> = (
|
||||
search,
|
||||
filteredItems
|
||||
) => {
|
||||
// If there is exact match for entity id, put it first
|
||||
const index = filteredItems.findIndex(
|
||||
(item) => item.stateObj?.entity_id === search
|
||||
);
|
||||
if (index === -1) {
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
const [exactMatch] = filteredItems.splice(index, 1);
|
||||
filteredItems.unshift(exactMatch);
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
|
@@ -25,7 +25,10 @@ import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-input-helper-text";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import type {
|
||||
PickerComboBoxItem,
|
||||
PickerComboBoxSearchFn,
|
||||
} from "../ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
@@ -470,6 +473,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
.getItems=${this._getItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
.searchFn=${this._searchFn}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
@@ -477,6 +481,24 @@ export class HaStatisticPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _searchFn: PickerComboBoxSearchFn<StatisticComboBoxItem> = (
|
||||
search,
|
||||
filteredItems
|
||||
) => {
|
||||
// If there is exact match for entity id or statistic id, put it first
|
||||
const index = filteredItems.findIndex(
|
||||
(item) =>
|
||||
item.stateObj?.entity_id === search || item.statistic_id === search
|
||||
);
|
||||
if (index === -1) {
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
const [exactMatch] = filteredItems.splice(index, 1);
|
||||
filteredItems.unshift(exactMatch);
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
@@ -509,7 +509,7 @@ export class HaAssistChat extends LitElement {
|
||||
this.requestUpdate("_conversation");
|
||||
},
|
||||
processEvent: (event: PipelineRunEvent) => {
|
||||
if (event.type === "intent-progress") {
|
||||
if (event.type === "intent-progress" && event.data.chat_log_delta) {
|
||||
const delta = event.data.chat_log_delta;
|
||||
|
||||
// new message
|
||||
|
@@ -345,8 +345,10 @@ export class HaComboBox extends LitElement {
|
||||
// @ts-ignore
|
||||
this._comboBox._closeOnBlurIsPrevented = true;
|
||||
}
|
||||
if (!this.opened) {
|
||||
return;
|
||||
}
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (newValue !== this.value) {
|
||||
fireEvent(this, "value-changed", { value: newValue || undefined });
|
||||
}
|
||||
|
@@ -34,6 +34,8 @@ const getWarning = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
||||
export class HaForm extends LitElement implements HaFormElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public data!: HaFormDataContainer;
|
||||
|
||||
@property({ attribute: false }) public schema!: readonly HaFormSchema[];
|
||||
@@ -135,6 +137,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
? html`<ha-selector
|
||||
.schema=${item}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.name=${item.name}
|
||||
.selector=${item.selector}
|
||||
.value=${getValue(this.data, item)}
|
||||
|
@@ -12,6 +12,7 @@ import "./ha-picker-combo-box";
|
||||
import type {
|
||||
HaPickerComboBox,
|
||||
PickerComboBoxItem,
|
||||
PickerComboBoxSearchFn,
|
||||
} from "./ha-picker-combo-box";
|
||||
import "./ha-picker-field";
|
||||
import type { HaPickerField, PickerValueRenderer } from "./ha-picker-field";
|
||||
@@ -57,6 +58,9 @@ export class HaGenericPicker extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public valueRenderer?: PickerValueRenderer;
|
||||
|
||||
@property({ attribute: false })
|
||||
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||
|
||||
@property({ attribute: "not-found-label", type: String })
|
||||
public notFoundLabel?: string;
|
||||
|
||||
@@ -68,7 +72,9 @@ export class HaGenericPicker extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
${this.label
|
||||
? html`<label ?disabled=${this.disabled}>${this.label}</label>`
|
||||
: nothing}
|
||||
<div class="container">
|
||||
${!this._opened
|
||||
? html`
|
||||
@@ -102,16 +108,19 @@ export class HaGenericPicker extends LitElement {
|
||||
.notFoundLabel=${this.notFoundLabel}
|
||||
.getItems=${this.getItems}
|
||||
.getAdditionalItems=${this.getAdditionalItems}
|
||||
.searchFn=${this.searchFn}
|
||||
></ha-picker-combo-box>
|
||||
`}
|
||||
${this._renderHelper()}
|
||||
</div>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
@@ -160,10 +169,17 @@ export class HaGenericPicker extends LitElement {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
label[disabled] {
|
||||
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
ha-input-helper-text {
|
||||
display: block;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -2,34 +2,9 @@ import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { CustomIcon } from "../data/custom_icons";
|
||||
import { customIcons } from "../data/custom_icons";
|
||||
import type { Chunks, Icons } from "../data/iconsets";
|
||||
import {
|
||||
MDI_PREFIXES,
|
||||
findIconChunk,
|
||||
getIcon,
|
||||
writeCache,
|
||||
} from "../data/iconsets";
|
||||
import { loadIcon } from "../data/load_icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
type DeprecatedIcon = Record<
|
||||
string,
|
||||
{
|
||||
removeIn: string;
|
||||
newName?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
const mdiDeprecatedIcons: DeprecatedIcon = {};
|
||||
|
||||
const chunks: Chunks = {};
|
||||
|
||||
const debouncedWriteCache = debounce(() => writeCache(chunks), 2000);
|
||||
|
||||
const cachedIcons: Record<string, string> = {};
|
||||
|
||||
@customElement("ha-icon")
|
||||
export class HaIcon extends LitElement {
|
||||
@property() public icon?: string;
|
||||
@@ -71,118 +46,24 @@ export class HaIcon extends LitElement {
|
||||
if (!this.icon) {
|
||||
return;
|
||||
}
|
||||
const requestedIcon = this.icon;
|
||||
const [iconPrefix, origIconName] = this.icon.split(":", 2);
|
||||
const result = await loadIcon(this.icon, this._handleWarning);
|
||||
|
||||
let iconName = origIconName;
|
||||
|
||||
if (!iconPrefix || !iconName) {
|
||||
if (result.icon !== this.icon) {
|
||||
// The icon was changed while we were loading it, so we don't update the state
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MDI_PREFIXES.includes(iconPrefix)) {
|
||||
const customIcon = customIcons[iconPrefix];
|
||||
if (customIcon) {
|
||||
if (customIcon && typeof customIcon.getIcon === "function") {
|
||||
this._setCustomPath(customIcon.getIcon(iconName), requestedIcon);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._legacy = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this._legacy = false;
|
||||
|
||||
if (iconName in mdiDeprecatedIcons) {
|
||||
const deprecatedIcon = mdiDeprecatedIcons[iconName];
|
||||
let message: string;
|
||||
|
||||
if (deprecatedIcon.newName) {
|
||||
message = `Icon ${iconPrefix}:${iconName} was renamed to ${iconPrefix}:${deprecatedIcon.newName}, please change your config, it will be removed in version ${deprecatedIcon.removeIn}.`;
|
||||
iconName = deprecatedIcon.newName!;
|
||||
} else {
|
||||
message = `Icon ${iconPrefix}:${iconName} was removed from MDI, please replace this icon with an other icon in your config, it will be removed in version ${deprecatedIcon.removeIn}.`;
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(message);
|
||||
fireEvent(this, "write_log", {
|
||||
level: "warning",
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
if (iconName in cachedIcons) {
|
||||
this._path = cachedIcons[iconName];
|
||||
return;
|
||||
}
|
||||
|
||||
if (iconName === "home-assistant") {
|
||||
const icon = (await import("../resources/home-assistant-logo-svg"))
|
||||
.mdiHomeAssistant;
|
||||
|
||||
if (this.icon === requestedIcon) {
|
||||
this._path = icon;
|
||||
}
|
||||
cachedIcons[iconName] = icon;
|
||||
return;
|
||||
}
|
||||
|
||||
let databaseIcon: string | undefined;
|
||||
try {
|
||||
databaseIcon = await getIcon(iconName);
|
||||
} catch (_err) {
|
||||
// Firefox in private mode doesn't support IDB
|
||||
// iOS Safari sometimes doesn't open the DB
|
||||
databaseIcon = undefined;
|
||||
}
|
||||
|
||||
if (databaseIcon) {
|
||||
if (this.icon === requestedIcon) {
|
||||
this._path = databaseIcon;
|
||||
}
|
||||
cachedIcons[iconName] = databaseIcon;
|
||||
return;
|
||||
}
|
||||
const chunk = findIconChunk(iconName);
|
||||
|
||||
if (chunk in chunks) {
|
||||
this._setPath(chunks[chunk], iconName, requestedIcon);
|
||||
return;
|
||||
}
|
||||
|
||||
const iconPromise = fetch(`/static/mdi/${chunk}.json`).then((response) =>
|
||||
response.json()
|
||||
);
|
||||
chunks[chunk] = iconPromise;
|
||||
this._setPath(iconPromise, iconName, requestedIcon);
|
||||
debouncedWriteCache();
|
||||
this._legacy = result.legacy || false;
|
||||
this._path = result.path;
|
||||
this._secondaryPath = result.secondaryPath;
|
||||
this._viewBox = result.viewBox;
|
||||
}
|
||||
|
||||
private async _setCustomPath(
|
||||
promise: Promise<CustomIcon>,
|
||||
requestedIcon: string
|
||||
) {
|
||||
const icon = await promise;
|
||||
if (this.icon !== requestedIcon) {
|
||||
return;
|
||||
}
|
||||
this._path = icon.path;
|
||||
this._secondaryPath = icon.secondaryPath;
|
||||
this._viewBox = icon.viewBox;
|
||||
}
|
||||
|
||||
private async _setPath(
|
||||
promise: Promise<Icons>,
|
||||
iconName: string,
|
||||
requestedIcon: string
|
||||
) {
|
||||
const iconPack = await promise;
|
||||
if (this.icon === requestedIcon) {
|
||||
this._path = iconPack[iconName];
|
||||
}
|
||||
cachedIcons[iconName] = iconPack[iconName];
|
||||
}
|
||||
private _handleWarning = (message: string) => {
|
||||
fireEvent(this, "write_log", {
|
||||
level: "warning",
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
|
@@ -1,9 +1,11 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-input-helper-text")
|
||||
class InputHelperText extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
@@ -18,6 +20,9 @@ class InputHelperText extends LitElement {
|
||||
padding-inline-start: 16px;
|
||||
padding-inline-end: 16px;
|
||||
}
|
||||
:host([disabled]) {
|
||||
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -262,7 +262,7 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
];
|
||||
}
|
||||
|
||||
return items.sort((a, b) =>
|
||||
return visibleItems.sort((a, b) =>
|
||||
a.disableSorting && !b.disableSorting ? -1 : compare(a.value, b.value)
|
||||
);
|
||||
}
|
||||
|
@@ -77,7 +77,7 @@ export class HaMarkdown extends LitElement {
|
||||
pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
line-height: 1.45;
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
font-family: var(--ha-font-family-code);
|
||||
}
|
||||
h1,
|
||||
|
@@ -85,7 +85,9 @@ class HaMultiTextField extends LitElement {
|
||||
</ha-button>
|
||||
</div>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
@@ -49,6 +49,12 @@ const DEFAULT_ROW_RENDERER: ComboBoxLitRenderer<PickerComboBoxItem> = (
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = (
|
||||
search: string,
|
||||
filteredItems: T[],
|
||||
allItems: T[]
|
||||
) => T[];
|
||||
|
||||
@customElement("ha-picker-combo-box")
|
||||
export class HaPickerComboBox extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -84,6 +90,9 @@ export class HaPickerComboBox extends LitElement {
|
||||
@property({ attribute: "not-found-label", type: String })
|
||||
public notFoundLabel?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
@@ -237,6 +246,7 @@ export class HaPickerComboBox extends LitElement {
|
||||
const fuse = new HaFuse(this._items, { shouldSort: false }, index);
|
||||
|
||||
const results = fuse.multiTermsSearch(searchString);
|
||||
let filteredItems = this._items as PickerComboBoxItem[];
|
||||
if (results) {
|
||||
const items = results.map((result) => result.item);
|
||||
if (items.length === 0) {
|
||||
@@ -246,10 +256,14 @@ export class HaPickerComboBox extends LitElement {
|
||||
}
|
||||
const additionalItems = this._getAdditionalItems(searchString);
|
||||
items.push(...additionalItems);
|
||||
target.filteredItems = items;
|
||||
} else {
|
||||
target.filteredItems = this._items;
|
||||
filteredItems = items;
|
||||
}
|
||||
|
||||
if (this.searchFn) {
|
||||
filteredItems = this.searchFn(searchString, filteredItems, this._items);
|
||||
}
|
||||
|
||||
target.filteredItems = filteredItems;
|
||||
}
|
||||
|
||||
private _setValue(value: string | undefined) {
|
||||
|
@@ -88,6 +88,12 @@ export class HaPickerField extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
ha-combo-box-item[disabled] {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-fill-color,
|
||||
whitesmoke
|
||||
);
|
||||
}
|
||||
ha-combo-box-item {
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
border-radius: 4px;
|
||||
@@ -95,8 +101,8 @@ export class HaPickerField extends LitElement {
|
||||
border-end-start-radius: 0;
|
||||
--md-list-item-one-line-container-height: 56px;
|
||||
--md-list-item-two-line-container-height: 56px;
|
||||
--md-list-item-top-space: 8px;
|
||||
--md-list-item-bottom-space: 8px;
|
||||
--md-list-item-top-space: 0px;
|
||||
--md-list-item-bottom-space: 0px;
|
||||
--md-list-item-leading-space: 8px;
|
||||
--md-list-item-trailing-space: 8px;
|
||||
--ha-md-list-item-gap: 8px;
|
||||
@@ -106,6 +112,12 @@ export class HaPickerField extends LitElement {
|
||||
}
|
||||
|
||||
/* Add Similar focus style as the text field */
|
||||
ha-combo-box-item[disabled]:after {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
}
|
||||
ha-combo-box-item:after {
|
||||
display: block;
|
||||
content: "";
|
||||
|
@@ -19,6 +19,8 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
export class HaActionSelector extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public selector!: ActionSelector;
|
||||
|
||||
@property({ attribute: false }) public value?: Action;
|
||||
@@ -66,6 +68,7 @@ export class HaActionSelector extends SubscribeMixin(LitElement) {
|
||||
.disabled=${this.disabled}
|
||||
.actions=${this._actions(this.value)}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-automation-action>
|
||||
`;
|
||||
}
|
||||
|
@@ -9,6 +9,8 @@ import type { HomeAssistant } from "../../types";
|
||||
export class HaConditionSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public selector!: ConditionSelector;
|
||||
|
||||
@property({ attribute: false }) public value?: Condition;
|
||||
@@ -24,6 +26,7 @@ export class HaConditionSelector extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.conditions=${this.value || []}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-automation-condition>
|
||||
`;
|
||||
}
|
||||
|
@@ -11,6 +11,8 @@ import type { HomeAssistant } from "../../types";
|
||||
export class HaTriggerSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public selector!: TriggerSelector;
|
||||
|
||||
@property({ attribute: false }) public value?: Trigger;
|
||||
@@ -33,6 +35,7 @@ export class HaTriggerSelector extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.triggers=${this._triggers(this.value)}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-automation-trigger>
|
||||
`;
|
||||
}
|
||||
|
@@ -69,6 +69,8 @@ const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]);
|
||||
export class HaSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property() public name?: string;
|
||||
|
||||
@property({ attribute: false }) public selector!: Selector;
|
||||
@@ -127,6 +129,7 @@ export class HaSelector extends LitElement {
|
||||
return html`
|
||||
${dynamicElement(`ha-selector-${this._type}`, {
|
||||
hass: this.hass,
|
||||
narrow: this.narrow,
|
||||
name: this.name,
|
||||
selector: this._handleLegacySelector(this.selector),
|
||||
value: this.value,
|
||||
|
@@ -85,8 +85,11 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: "show-advanced", type: Boolean }) public showAdvanced =
|
||||
false;
|
||||
@property({ attribute: "show-advanced", type: Boolean })
|
||||
public showAdvanced = false;
|
||||
|
||||
@property({ attribute: "show-service-id", type: Boolean })
|
||||
public showServiceId = false;
|
||||
|
||||
@property({ attribute: "hide-picker", type: Boolean, reflect: true })
|
||||
public hidePicker = false;
|
||||
@@ -435,6 +438,7 @@ export class HaServiceControl extends LitElement {
|
||||
.value=${this._value?.action}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._serviceChanged}
|
||||
.showServiceId=${this.showServiceId}
|
||||
></ha-service-picker>`}
|
||||
${this.hideDescription
|
||||
? nothing
|
||||
|
@@ -1,15 +1,25 @@
|
||||
import { mdiRoomService } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { html, LitElement, nothing, type TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { isValidServiceId } from "../common/entity/valid_service_id";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { domainToName } from "../data/integration";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-service-icon";
|
||||
import { getServiceIcons } from "../data/icons";
|
||||
import { domainToName } from "../data/integration";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||
import "./ha-service-icon";
|
||||
|
||||
interface ServiceComboBoxItem extends PickerComboBoxItem {
|
||||
domain_name?: string;
|
||||
service_id?: string;
|
||||
}
|
||||
|
||||
@customElement("ha-service-picker")
|
||||
class HaServicePicker extends LitElement {
|
||||
@@ -17,66 +27,121 @@ class HaServicePicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@state() private _filter?: string;
|
||||
@property({ attribute: "show-service-id", type: Boolean })
|
||||
public showServiceId = false;
|
||||
|
||||
protected willUpdate() {
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadBackendTranslation("services");
|
||||
getServiceIcons(this.hass);
|
||||
}
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> =
|
||||
(item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<ha-service-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.service=${item.service}
|
||||
></ha-service-icon>
|
||||
<span slot="headline">${item.name}</span>
|
||||
<span slot="supporting-text"
|
||||
>${item.name === item.service ? "" : item.service}</span
|
||||
>
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
protected firstUpdated(props) {
|
||||
super.firstUpdated(props);
|
||||
this.hass.loadBackendTranslation("services");
|
||||
getServiceIcons(this.hass);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
private _rowRenderer: ComboBoxLitRenderer<ServiceComboBoxItem> = (
|
||||
item,
|
||||
{ index }
|
||||
) => html`
|
||||
<ha-combo-box-item type="button" border-top .borderTop=${index !== 0}>
|
||||
<ha-service-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.components.service-picker.action")}
|
||||
.filteredItems=${this._filteredServices(
|
||||
this.hass.localize,
|
||||
this.hass.services,
|
||||
this._filter
|
||||
)}
|
||||
.value=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
.renderer=${this._rowRenderer}
|
||||
item-value-path="service"
|
||||
item-label-path="name"
|
||||
.service=${item.id}
|
||||
></ha-service-icon>
|
||||
<span slot="headline">${item.primary}</span>
|
||||
<span slot="supporting-text">${item.secondary}</span>
|
||||
${item.service_id && this.showServiceId
|
||||
? html`<span slot="supporting-text" class="code">
|
||||
${item.service_id}
|
||||
</span>`
|
||||
: nothing}
|
||||
${item.domain_name
|
||||
? html`
|
||||
<div slot="trailing-supporting-text" class="domain">
|
||||
${item.domain_name}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = (value) => {
|
||||
const serviceId = value;
|
||||
const [domain, service] = serviceId.split(".");
|
||||
|
||||
if (!this.hass.services[domain]?.[service]) {
|
||||
return html`
|
||||
<ha-svg-icon slot="start" .path=${mdiRoomService}></ha-svg-icon>
|
||||
<span slot="headline">${value}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
const serviceName =
|
||||
this.hass.localize(`component.${domain}.services.${service}.name`) ||
|
||||
this.hass.services[domain][service].name ||
|
||||
service;
|
||||
|
||||
return html`
|
||||
<ha-service-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.service=${serviceId}
|
||||
></ha-service-icon>
|
||||
<span slot="headline">${serviceName}</span>
|
||||
${this.showServiceId
|
||||
? html`<span slot="supporting-text" class="code">${serviceId}</span>`
|
||||
: nothing}
|
||||
`;
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const placeholder =
|
||||
this.placeholder ??
|
||||
this.hass.localize("ui.components.service-picker.action");
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
allow-custom-value
|
||||
@filter-changed=${this._filterChanged}
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.service-picker.no_match"
|
||||
)}
|
||||
.label=${this.label}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
.getItems=${this._getItems}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-combo-box>
|
||||
>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getItems = () =>
|
||||
this._services(this.hass.localize, this.hass.services);
|
||||
|
||||
private _services = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"]
|
||||
): {
|
||||
service: string;
|
||||
name: string;
|
||||
}[] => {
|
||||
): ServiceComboBoxItem[] => {
|
||||
if (!services) {
|
||||
return [];
|
||||
}
|
||||
const result: { service: string; name: string }[] = [];
|
||||
const items: ServiceComboBoxItem[] = [];
|
||||
|
||||
Object.keys(services)
|
||||
.sort()
|
||||
@@ -84,56 +149,60 @@ class HaServicePicker extends LitElement {
|
||||
const services_keys = Object.keys(services[domain]).sort();
|
||||
|
||||
for (const service of services_keys) {
|
||||
result.push({
|
||||
service: `${domain}.${service}`,
|
||||
name: `${domainToName(localize, domain)}: ${
|
||||
this.hass.localize(
|
||||
`component.${domain}.services.${service}.name`
|
||||
) ||
|
||||
services[domain][service].name ||
|
||||
service
|
||||
}`,
|
||||
const serviceId = `${domain}.${service}`;
|
||||
const domainName = domainToName(localize, domain);
|
||||
|
||||
const name =
|
||||
this.hass.localize(
|
||||
`component.${domain}.services.${service}.name`
|
||||
) ||
|
||||
services[domain][service].name ||
|
||||
service;
|
||||
|
||||
const description =
|
||||
this.hass.localize(
|
||||
`component.${domain}.services.${service}.description`
|
||||
) || services[domain][service].description;
|
||||
|
||||
items.push({
|
||||
id: serviceId,
|
||||
primary: name,
|
||||
secondary: description,
|
||||
domain_name: domainName,
|
||||
service_id: serviceId,
|
||||
search_labels: [serviceId, domainName, name, description].filter(
|
||||
Boolean
|
||||
),
|
||||
sorting_label: serviceId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _filteredServices = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"],
|
||||
filter?: string
|
||||
) => {
|
||||
if (!services) {
|
||||
return [];
|
||||
}
|
||||
const processedServices = this._services(localize, services);
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (!filter) {
|
||||
return processedServices;
|
||||
}
|
||||
const split_filter = filter.split(" ");
|
||||
return processedServices.filter((service) => {
|
||||
const lower_service_name = service.name.toLowerCase();
|
||||
const lower_service = service.service.toLowerCase();
|
||||
return split_filter.every(
|
||||
(f) => lower_service_name.includes(f) || lower_service.includes(f)
|
||||
);
|
||||
});
|
||||
if (!value) {
|
||||
this._setValue(undefined);
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
this._filter = ev.detail.value.toLowerCase();
|
||||
if (!isValidServiceId(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._setValue(value);
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
this.value = ev.detail.value;
|
||||
private _setValue(value: string | undefined) {
|
||||
this.value = value;
|
||||
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -368,7 +368,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
if (!this._panelOrder || !this._hiddenPanels) {
|
||||
return html`
|
||||
<ha-fade-in .delay=${500}
|
||||
><ha-spinner size="large"></ha-spinner
|
||||
><ha-spinner size="small"></ha-spinner
|
||||
></ha-fade-in>
|
||||
`;
|
||||
}
|
||||
@@ -626,12 +626,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
this._tooltipHideTimeout = undefined;
|
||||
}
|
||||
const tooltip = this._tooltip;
|
||||
const listbox = this.shadowRoot!.querySelector("ha-md-list")!;
|
||||
let top = item.offsetTop + 11;
|
||||
if (listbox.contains(item)) {
|
||||
top += listbox.offsetTop;
|
||||
top -= listbox.scrollTop;
|
||||
}
|
||||
const allListbox = this.shadowRoot!.querySelectorAll("ha-md-list")!;
|
||||
const listbox = [...allListbox].find((lb) => lb.contains(item));
|
||||
|
||||
const top =
|
||||
item.offsetTop +
|
||||
11 +
|
||||
(listbox?.offsetTop ?? 0) -
|
||||
(listbox?.scrollTop ?? 0);
|
||||
|
||||
tooltip.innerText = (
|
||||
item.querySelector(".item-text") as HTMLElement
|
||||
).innerText;
|
||||
|
@@ -12,6 +12,8 @@ class HaEntityMarker extends LitElement {
|
||||
|
||||
@property({ attribute: "entity-name" }) public entityName?: string;
|
||||
|
||||
@property({ attribute: "entity-unit" }) public entityUnit?: string;
|
||||
|
||||
@property({ attribute: "entity-picture" }) public entityPicture?: string;
|
||||
|
||||
@property({ attribute: "entity-color" }) public entityColor?: string;
|
||||
@@ -37,7 +39,16 @@ class HaEntityMarker extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.hass?.states[this.entityId]}
|
||||
></ha-state-icon>`
|
||||
: this.entityName}
|
||||
: !this.entityUnit
|
||||
? this.entityName
|
||||
: html`
|
||||
${this.entityName}
|
||||
<span
|
||||
class="unit"
|
||||
style="display: ${this.entityUnit ? "initial" : "none"}"
|
||||
>${this.entityUnit}</span
|
||||
>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -72,6 +83,9 @@ class HaEntityMarker extends LitElement {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.unit {
|
||||
margin-left: 2px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -56,6 +56,7 @@ export interface HaMapEntity {
|
||||
color: string;
|
||||
label_mode?: "name" | "state" | "attribute" | "icon";
|
||||
attribute?: string;
|
||||
unit?: string;
|
||||
name?: string;
|
||||
focus?: boolean;
|
||||
}
|
||||
@@ -549,6 +550,12 @@ export class HaMap extends ReactiveElement {
|
||||
typeof entity !== "string" && entity.label_mode === "icon";
|
||||
entityMarker.entityId = getEntityId(entity);
|
||||
entityMarker.entityName = entityName;
|
||||
entityMarker.entityUnit =
|
||||
typeof entity !== "string" &&
|
||||
entity.unit &&
|
||||
entity.label_mode === "attribute"
|
||||
? entity.unit
|
||||
: "";
|
||||
entityMarker.entityPicture =
|
||||
entityPicture && (typeof entity === "string" || !entity.label_mode)
|
||||
? this.hass.hassUrl(entityPicture)
|
||||
|
@@ -134,7 +134,8 @@ export interface ConversationChatLogToolResultDelta {
|
||||
interface PipelineIntentProgressEvent extends PipelineEventBase {
|
||||
type: "intent-progress";
|
||||
data: {
|
||||
chat_log_delta:
|
||||
tts_start_streaming?: boolean;
|
||||
chat_log_delta?:
|
||||
| Partial<ConversationChatLogAssistantDelta>
|
||||
// These always come in 1 chunk
|
||||
| ConversationChatLogToolResultDelta;
|
||||
|
@@ -26,6 +26,7 @@ import {
|
||||
import type { EntityRegistryEntry } from "./entity_registry";
|
||||
import type { FrontendLocaleData } from "./translation";
|
||||
import { isTriggerList } from "./trigger";
|
||||
import { hasTemplate } from "../common/string/has-template";
|
||||
|
||||
const triggerTranslationBaseKey =
|
||||
"ui.panel.config.automation.editor.triggers.type";
|
||||
@@ -820,6 +821,12 @@ const tryDescribeCondition = (
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
ignoreAlias = false
|
||||
) => {
|
||||
if (typeof condition === "string" && hasTemplate(condition)) {
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.template.description.full`
|
||||
);
|
||||
}
|
||||
|
||||
if (condition.alias && !ignoreAlias) {
|
||||
return condition.alias;
|
||||
}
|
||||
|
@@ -339,7 +339,7 @@ export const computeBackupSize = (backup: BackupContent) =>
|
||||
|
||||
export type BackupType = "automatic" | "manual" | "addon_update";
|
||||
|
||||
const BACKUP_TYPE_ORDER: BackupType[] = ["automatic", "manual", "addon_update"];
|
||||
const BACKUP_TYPE_ORDER: BackupType[] = ["automatic", "addon_update", "manual"];
|
||||
|
||||
export const getBackupTypes = memoize((isHassio: boolean) =>
|
||||
isHassio
|
||||
|
@@ -679,7 +679,9 @@ export const getEnergyDataCollection = (
|
||||
const period =
|
||||
preferredPeriod === "today" && hour === "0" ? "yesterday" : preferredPeriod;
|
||||
|
||||
[collection.start, collection.end] = calcDateRange(hass, period);
|
||||
const [start, end] = calcDateRange(hass, period);
|
||||
collection.start = calcDate(start, startOfDay, hass.locale, hass.config);
|
||||
collection.end = calcDate(end, endOfDay, hass.locale, hass.config);
|
||||
|
||||
const scheduleUpdatePeriod = () => {
|
||||
collection._updatePeriodTimeout = window.setTimeout(
|
||||
|
@@ -640,6 +640,12 @@ export const mergeHistoryResults = (
|
||||
}
|
||||
|
||||
for (const item of ltsResult.line) {
|
||||
if (item.unit === BLANK_UNIT) {
|
||||
// disabled entities have no unit, so we need to find the unit from the history result
|
||||
item.unit =
|
||||
historyResult.line.find((line) => line.identifier === item.identifier)
|
||||
?.unit ?? BLANK_UNIT;
|
||||
}
|
||||
const key = computeGroupKey(
|
||||
item.unit,
|
||||
item.device_class,
|
||||
|
142
src/data/load_icon.ts
Normal file
142
src/data/load_icon.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { customIcons } from "./custom_icons";
|
||||
import {
|
||||
findIconChunk,
|
||||
getIcon,
|
||||
MDI_PREFIXES,
|
||||
writeCache,
|
||||
type Chunks,
|
||||
} from "./iconsets";
|
||||
|
||||
interface IconLoadResult {
|
||||
icon: string;
|
||||
legacy?: boolean;
|
||||
path?: string;
|
||||
secondaryPath?: string;
|
||||
viewBox?: string;
|
||||
}
|
||||
|
||||
type DeprecatedIcon = Record<
|
||||
string,
|
||||
{
|
||||
removeIn: string;
|
||||
newName?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
const chunks: Chunks = {};
|
||||
|
||||
const debouncedWriteCache = debounce(() => writeCache(chunks), 2000);
|
||||
|
||||
const cachedIcons: Record<string, string> = {};
|
||||
|
||||
const mdiDeprecatedIcons: DeprecatedIcon = {};
|
||||
|
||||
export const loadIcon = async (
|
||||
icon: string,
|
||||
warningCallback?: (message) => void
|
||||
): Promise<IconLoadResult> => {
|
||||
const [iconPrefix, origIconName] = icon.split(":", 2);
|
||||
|
||||
let iconName = origIconName;
|
||||
|
||||
if (!iconPrefix || !iconName) {
|
||||
return {
|
||||
icon,
|
||||
};
|
||||
}
|
||||
|
||||
if (!MDI_PREFIXES.includes(iconPrefix)) {
|
||||
const customIcon = customIcons[iconPrefix];
|
||||
if (customIcon) {
|
||||
if (customIcon && typeof customIcon.getIcon === "function") {
|
||||
const custom = await customIcon.getIcon(iconName);
|
||||
return {
|
||||
icon,
|
||||
path: custom.path,
|
||||
secondaryPath: custom.secondaryPath,
|
||||
viewBox: custom.viewBox,
|
||||
};
|
||||
}
|
||||
return {
|
||||
icon,
|
||||
};
|
||||
}
|
||||
return {
|
||||
icon,
|
||||
legacy: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (iconName in mdiDeprecatedIcons) {
|
||||
const deprecatedIcon = mdiDeprecatedIcons[iconName];
|
||||
let message: string;
|
||||
|
||||
if (deprecatedIcon.newName) {
|
||||
message = `Icon ${iconPrefix}:${iconName} was renamed to ${iconPrefix}:${deprecatedIcon.newName}, please change your config, it will be removed in version ${deprecatedIcon.removeIn}.`;
|
||||
iconName = deprecatedIcon.newName!;
|
||||
} else {
|
||||
message = `Icon ${iconPrefix}:${iconName} was removed from MDI, please replace this icon with an other icon in your config, it will be removed in version ${deprecatedIcon.removeIn}.`;
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(message);
|
||||
if (warningCallback) {
|
||||
warningCallback(message);
|
||||
}
|
||||
}
|
||||
|
||||
if (iconName in cachedIcons) {
|
||||
return {
|
||||
icon,
|
||||
path: cachedIcons[iconName],
|
||||
};
|
||||
}
|
||||
|
||||
if (iconName === "home-assistant") {
|
||||
const ha = (await import("../resources/home-assistant-logo-svg"))
|
||||
.mdiHomeAssistant;
|
||||
|
||||
cachedIcons[iconName] = ha;
|
||||
return {
|
||||
icon,
|
||||
path: ha,
|
||||
};
|
||||
}
|
||||
|
||||
let databaseIcon: string | undefined;
|
||||
try {
|
||||
databaseIcon = await getIcon(iconName);
|
||||
} catch (_err) {
|
||||
// Firefox in private mode doesn't support IDB
|
||||
// iOS Safari sometimes doesn't open the DB
|
||||
databaseIcon = undefined;
|
||||
}
|
||||
|
||||
if (databaseIcon) {
|
||||
cachedIcons[iconName] = databaseIcon;
|
||||
return {
|
||||
icon,
|
||||
path: databaseIcon,
|
||||
};
|
||||
}
|
||||
const chunk = findIconChunk(iconName);
|
||||
|
||||
if (chunk in chunks) {
|
||||
const iconPack = await chunks[chunk];
|
||||
return {
|
||||
icon,
|
||||
path: iconPack[iconName],
|
||||
};
|
||||
}
|
||||
|
||||
const iconPromise = fetch(`/static/mdi/${chunk}.json`).then((response) =>
|
||||
response.json()
|
||||
);
|
||||
chunks[chunk] = iconPromise;
|
||||
debouncedWriteCache();
|
||||
const iconPack = await iconPromise;
|
||||
return {
|
||||
icon,
|
||||
path: iconPack[iconName],
|
||||
};
|
||||
};
|
@@ -6,6 +6,7 @@ const HAS_CUSTOM_PREVIEW = ["generic_camera", "template"];
|
||||
export interface GenericPreview {
|
||||
state: string;
|
||||
attributes: Record<string, any>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const subscribePreviewGeneric = (
|
||||
|
@@ -29,6 +29,7 @@ import { migrateAutomationTrigger } from "./automation";
|
||||
import type { BlueprintInput } from "./blueprint";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
import { createSearchParam } from "../common/url/search-params";
|
||||
import { hasTemplate } from "../common/string/has-template";
|
||||
|
||||
export const MODES = ["single", "restart", "queued", "parallel"] as const;
|
||||
export const MODES_MAX = ["queued", "parallel"] as const;
|
||||
@@ -339,6 +340,9 @@ export const getScriptEditorInitData = () => {
|
||||
|
||||
export const getActionType = (action: Action): ActionType => {
|
||||
// Check based on config_validation.py#determine_script_action
|
||||
if (typeof action === "string" && hasTemplate(action)) {
|
||||
return "check_condition";
|
||||
}
|
||||
if ("delay" in action) {
|
||||
return "delay";
|
||||
}
|
||||
|
@@ -349,6 +349,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
${this._step.type === "form"
|
||||
? html`
|
||||
<step-flow-form
|
||||
narrow
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
|
@@ -58,6 +58,11 @@ export class FlowPreviewGeneric extends LitElement {
|
||||
}
|
||||
|
||||
private _setPreview = (preview: GenericPreview) => {
|
||||
if (preview.error) {
|
||||
this._error = preview.error;
|
||||
this._preview = undefined;
|
||||
return;
|
||||
}
|
||||
const now = new Date().toISOString();
|
||||
this._preview = {
|
||||
entity_id: `${this.stepId}.___flow_preview___`,
|
||||
@@ -80,6 +85,7 @@ export class FlowPreviewGeneric extends LitElement {
|
||||
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
|
||||
return;
|
||||
}
|
||||
this._error = undefined;
|
||||
try {
|
||||
this._unsub = subscribePreviewGeneric(
|
||||
this.hass,
|
||||
@@ -89,6 +95,7 @@ export class FlowPreviewGeneric extends LitElement {
|
||||
this.stepData,
|
||||
this._setPreview
|
||||
);
|
||||
await this._unsub;
|
||||
fireEvent(this, "set-flow-errors", { errors: {} });
|
||||
} catch (err: any) {
|
||||
if (typeof err.message === "string") {
|
||||
|
@@ -235,9 +235,13 @@ class StepFlowCreateEntry extends LitElement {
|
||||
|
||||
fireEvent(this, "flow-update", { step: undefined });
|
||||
if (this.step.result && this.navigateToResult) {
|
||||
navigate(
|
||||
`/config/integrations/integration/${this.step.result.domain}#config_entry=${this.step.result.entry_id}`
|
||||
);
|
||||
if (this.devices.length === 1) {
|
||||
navigate(`/config/devices/device/${this.devices[0].id}`);
|
||||
} else {
|
||||
navigate(
|
||||
`/config/integrations/integration/${this.step.result.domain}#config_entry=${this.step.result.entry_id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -27,6 +27,8 @@ import { configFlowContentStyles } from "./styles";
|
||||
class StepFlowForm extends LitElement {
|
||||
@property({ attribute: false }) public flowConfig!: FlowConfig;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public step!: DataEntryFlowStepForm;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -63,6 +65,7 @@ class StepFlowForm extends LitElement {
|
||||
: ""}
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.data=${stepData}
|
||||
.disabled=${this._loading}
|
||||
@value-changed=${this._stepDataChanged}
|
||||
|
@@ -45,8 +45,7 @@ class MoreInfoCamera extends LitElement {
|
||||
<ha-progress-button
|
||||
@click=${this._downloadSnapshot}
|
||||
.progress=${this._waiting}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE ||
|
||||
this.stateObj.state === "idle"}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.camera.download_snapshot"
|
||||
|
@@ -497,9 +497,9 @@ export class HAFullCalendar extends LitElement {
|
||||
|
||||
ha-fab {
|
||||
position: absolute;
|
||||
bottom: 32px;
|
||||
right: 32px;
|
||||
inset-inline-end: 32px;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
inset-inline-end: 16px;
|
||||
inset-inline-start: initial;
|
||||
z-index: 1;
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js";
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -46,6 +45,7 @@ import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
|
||||
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
|
||||
import { HaFuse } from "../../../resources/fuse";
|
||||
|
||||
const TYPES = {
|
||||
trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS },
|
||||
@@ -175,6 +175,40 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
type: AddAutomationElementDialogParams["type"],
|
||||
group: string | undefined,
|
||||
filter: string,
|
||||
domains: Set<string> | undefined,
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"],
|
||||
manifests?: DomainManifestLookup
|
||||
): ListItem[] => {
|
||||
const items = this._items(type, group, localize, services, manifests);
|
||||
|
||||
const index = this._fuseIndex(items);
|
||||
|
||||
const fuse = new HaFuse(
|
||||
items,
|
||||
{ ignoreLocation: true, includeScore: true },
|
||||
index
|
||||
);
|
||||
|
||||
const results = fuse.multiTermsSearch(filter);
|
||||
if (results) {
|
||||
return results.map((result) => result.item);
|
||||
}
|
||||
return this._getGroupItems(
|
||||
type,
|
||||
group,
|
||||
domains,
|
||||
localize,
|
||||
services,
|
||||
manifests
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private _items = memoizeOne(
|
||||
(
|
||||
type: AddAutomationElementDialogParams["type"],
|
||||
group: string | undefined,
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"],
|
||||
manifests?: DomainManifestLookup
|
||||
@@ -189,24 +223,17 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
);
|
||||
|
||||
const items = flattenGroups(groups).flat();
|
||||
|
||||
if (type === "action") {
|
||||
items.push(...this._services(localize, services, manifests, group));
|
||||
}
|
||||
|
||||
const options: IFuseOptions<ListItem> = {
|
||||
keys: ["key", "name", "description"],
|
||||
isCaseSensitive: false,
|
||||
ignoreLocation: true,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.2,
|
||||
ignoreDiacritics: true,
|
||||
};
|
||||
const fuse = new Fuse(items, options);
|
||||
return fuse.search(filter).map((result) => result.item);
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _fuseIndex = memoizeOne((items: ListItem[]) =>
|
||||
Fuse.createIndex(["key", "name", "description"], items)
|
||||
);
|
||||
|
||||
private _getGroupItems = memoizeOne(
|
||||
(
|
||||
type: AddAutomationElementDialogParams["type"],
|
||||
@@ -449,6 +476,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
this._params.type,
|
||||
this._group,
|
||||
this._filter,
|
||||
this._domains,
|
||||
this.hass.localize,
|
||||
this.hass.services,
|
||||
this._manifests
|
||||
|
@@ -233,6 +233,8 @@ export class CloudLogin extends LitElement {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.cloud.login.cloud_pipeline_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.yes"),
|
||||
dismissText: this.hass.localize("ui.common.no"),
|
||||
})
|
||||
) {
|
||||
setAssistPipelinePreferred(this.hass, result.cloud_pipeline);
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
mdiDeleteForever,
|
||||
mdiHospitalBox,
|
||||
mdiInformation,
|
||||
mdiPlus,
|
||||
mdiUpload,
|
||||
mdiWrench,
|
||||
} from "@mdi/js";
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
fetchZwaveIntegrationSettings,
|
||||
fetchZwaveIsAnyOTAFirmwareUpdateInProgress,
|
||||
fetchZwaveIsNodeFirmwareUpdateInProgress,
|
||||
fetchZwaveNetworkStatus,
|
||||
fetchZwaveNodeStatus,
|
||||
} from "../../../../../../data/zwave_js";
|
||||
import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box";
|
||||
@@ -24,6 +26,7 @@ import { showZWaveJSRemoveFailedNodeDialog } from "../../../../integrations/inte
|
||||
import { showZWaveJSUpdateFirmwareNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-update-firmware-node";
|
||||
import type { DeviceAction } from "../../../ha-config-device-page";
|
||||
import { showZWaveJSHardResetControllerDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-hard-reset-controller";
|
||||
import { showZWaveJSAddNodeDialog } from "../../../../integrations/integration-panels/zwave_js/add-node/show-dialog-zwave_js-add-node";
|
||||
|
||||
export const getZwaveDeviceActions = async (
|
||||
el: HTMLElement,
|
||||
@@ -160,6 +163,19 @@ export const getZwaveDeviceActions = async (
|
||||
}
|
||||
|
||||
if (nodeStatus.is_controller_node) {
|
||||
const networkStatus = await fetchZwaveNetworkStatus(hass, {
|
||||
entry_id: entryId,
|
||||
});
|
||||
actions.unshift({
|
||||
label: hass.localize("ui.panel.config.zwave_js.common.add_node"),
|
||||
icon: mdiPlus,
|
||||
action: async () => {
|
||||
showZWaveJSAddNodeDialog(el, {
|
||||
entry_id: entryId,
|
||||
longRangeSupported: networkStatus.controller?.supports_long_range,
|
||||
});
|
||||
},
|
||||
});
|
||||
actions.push({
|
||||
label: hass.localize(
|
||||
"ui.panel.config.zwave_js.device_info.hard_reset_controller"
|
||||
|
@@ -127,16 +127,15 @@ export class HaConfigDevicePage extends LitElement {
|
||||
|
||||
@state() private _related?: RelatedResult;
|
||||
|
||||
// If a number, it's the request ID so we make sure we don't show older info
|
||||
@state() private _diagnosticDownloadLinks?: number | DeviceAction[];
|
||||
@state() private _diagnosticDownloadLinks: DeviceAction[] = [];
|
||||
|
||||
@state() private _deleteButtons?: DeviceAction[];
|
||||
@state() private _deleteButtons: DeviceAction[] = [];
|
||||
|
||||
@state() private _deviceActions?: DeviceAction[];
|
||||
@state() private _deviceActions: DeviceAction[] = [];
|
||||
|
||||
@state() private _deviceAlerts?: DeviceAlert[];
|
||||
@state() private _deviceAlerts: DeviceAlert[] = [];
|
||||
|
||||
private _deviceAlertsTimeout?: number;
|
||||
private _deviceAlertsActionsTimeout?: number;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
@@ -255,42 +254,19 @@ export class HaConfigDevicePage extends LitElement {
|
||||
public willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (
|
||||
changedProps.has("deviceId") ||
|
||||
changedProps.has("devices") ||
|
||||
changedProps.has("entries")
|
||||
) {
|
||||
this._diagnosticDownloadLinks = undefined;
|
||||
this._deleteButtons = undefined;
|
||||
this._deviceActions = undefined;
|
||||
this._deviceAlerts = undefined;
|
||||
if (changedProps.has("deviceId") || changedProps.has("entries")) {
|
||||
this._deviceActions = [];
|
||||
this._deviceAlerts = [];
|
||||
this._deleteButtons = [];
|
||||
this._diagnosticDownloadLinks = [];
|
||||
this._fetchData();
|
||||
}
|
||||
|
||||
if (
|
||||
(this._diagnosticDownloadLinks &&
|
||||
this._deleteButtons &&
|
||||
this._deviceActions &&
|
||||
this._deviceAlerts) ||
|
||||
!this.deviceId ||
|
||||
!this.entries
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._diagnosticDownloadLinks = Math.random();
|
||||
this._deleteButtons = []; // To prevent re-rendering if no delete buttons
|
||||
this._deviceActions = [];
|
||||
this._deviceAlerts = [];
|
||||
this._getDiagnosticButtons(this._diagnosticDownloadLinks);
|
||||
this._getDeleteActions();
|
||||
this._getDeviceActions();
|
||||
clearTimeout(this._deviceAlertsTimeout);
|
||||
this._getDeviceAlerts();
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
loadDeviceRegistryDetailDialog();
|
||||
this._fetchData();
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
@@ -302,7 +278,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
clearTimeout(this._deviceAlertsTimeout);
|
||||
clearTimeout(this._deviceAlertsActionsTimeout);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -909,7 +885,18 @@ export class HaConfigDevicePage extends LitElement {
|
||||
</hass-subpage>`;
|
||||
}
|
||||
|
||||
private async _getDiagnosticButtons(requestId: number): Promise<void> {
|
||||
private _fetchData() {
|
||||
if (this.deviceId && this.entries.length) {
|
||||
this._getDiagnosticButtons();
|
||||
this._getDeleteActions();
|
||||
clearTimeout(this._deviceAlertsActionsTimeout);
|
||||
this._getDeviceActions();
|
||||
this._getDeviceAlerts();
|
||||
}
|
||||
}
|
||||
|
||||
private async _getDiagnosticButtons(): Promise<void> {
|
||||
const deviceId = this.deviceId;
|
||||
if (!isComponentLoaded(this.hass, "diagnostics")) {
|
||||
return;
|
||||
}
|
||||
@@ -951,7 +938,8 @@ export class HaConfigDevicePage extends LitElement {
|
||||
|
||||
links = links.filter(Boolean);
|
||||
|
||||
if (this._diagnosticDownloadLinks !== requestId) {
|
||||
if (this.deviceId !== deviceId) {
|
||||
// abort if the device has changed
|
||||
return;
|
||||
}
|
||||
if (links.length > 0) {
|
||||
@@ -1176,12 +1164,12 @@ export class HaConfigDevicePage extends LitElement {
|
||||
deviceAlerts.push(...alerts);
|
||||
}
|
||||
|
||||
this._deviceAlerts = deviceAlerts;
|
||||
if (deviceAlerts.length) {
|
||||
this._deviceAlerts = deviceAlerts;
|
||||
this._deviceAlertsTimeout = window.setTimeout(
|
||||
() => this._getDeviceAlerts(),
|
||||
DEVICE_ALERTS_INTERVAL
|
||||
);
|
||||
this._deviceAlertsActionsTimeout = window.setTimeout(() => {
|
||||
this._getDeviceAlerts();
|
||||
this._getDeviceActions();
|
||||
}, DEVICE_ALERTS_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -163,6 +163,7 @@ export class HaIntegrationCard extends LitElement {
|
||||
: "custom"}"
|
||||
>
|
||||
<ha-tooltip
|
||||
hoist
|
||||
.placement=${computeRTL(this.hass) ? "right" : "left"}
|
||||
.content=${this.hass.localize(
|
||||
this.manifest.overwrites_built_in
|
||||
@@ -177,6 +178,7 @@ export class HaIntegrationCard extends LitElement {
|
||||
${this.manifest && this.manifest.iot_class?.startsWith("cloud_")
|
||||
? html`<div class="icon cloud">
|
||||
<ha-tooltip
|
||||
hoist
|
||||
.placement=${computeRTL(this.hass) ? "right" : "left"}
|
||||
.content=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.depends_on_cloud"
|
||||
@@ -191,6 +193,7 @@ export class HaIntegrationCard extends LitElement {
|
||||
!this.items.every((itm) => itm.source === "system")
|
||||
? html`<div class="icon yaml">
|
||||
<ha-tooltip
|
||||
hoist
|
||||
.placement=${computeRTL(this.hass) ? "right" : "left"}
|
||||
.content=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.no_config_flow"
|
||||
|
@@ -133,8 +133,11 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
if (ERROR_STATES.includes(this._configEntry.state)) {
|
||||
return this._renderErrorScreen();
|
||||
}
|
||||
const provisioningDevices =
|
||||
this._provisioningEntries?.filter((entry) => !entry.nodeId).length ?? 0;
|
||||
const notReadyDevices =
|
||||
this._network?.controller.nodes.filter((node) => !node.ready).length ?? 0;
|
||||
(this._network?.controller.nodes.filter((node) => !node.ready).length ??
|
||||
0) + provisioningDevices;
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
@@ -182,7 +185,9 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.dashboard.devices`,
|
||||
{
|
||||
count: this._network.controller.nodes.length,
|
||||
count:
|
||||
this._network.controller.nodes.length +
|
||||
provisioningDevices,
|
||||
}
|
||||
)}
|
||||
${notReadyDevices > 0
|
||||
@@ -424,7 +429,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
</ha-card>
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
<h1>Third-Party Data Reporting</h1>
|
||||
<h1>Third-party data reporting</h1>
|
||||
${this._dataCollectionOptIn !== undefined
|
||||
? html`
|
||||
<ha-switch
|
||||
|
@@ -142,6 +142,7 @@ class HaPanelDevAction extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.value=${this._serviceData?.action}
|
||||
@value-changed=${this._serviceChanged}
|
||||
show-service-id
|
||||
></ha-service-picker>
|
||||
<ha-yaml-editor
|
||||
id="yaml-editor"
|
||||
@@ -156,6 +157,7 @@ class HaPanelDevAction extends LitElement {
|
||||
.value=${this._serviceData}
|
||||
.narrow=${this.narrow}
|
||||
show-advanced
|
||||
show-service-id
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
class="card-content"
|
||||
></ha-service-control>
|
||||
|
@@ -130,6 +130,7 @@ class HaPanelDevState extends LitElement {
|
||||
.value=${this._entityId}
|
||||
@value-changed=${this._entityIdChanged}
|
||||
allow-custom-entity
|
||||
show-entity-id
|
||||
></ha-entity-picker>
|
||||
${this._entityId
|
||||
? html`
|
||||
|
@@ -64,7 +64,9 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
(source) => source.type === "solar"
|
||||
);
|
||||
const hasGas = prefs.energy_sources.some((source) => source.type === "gas");
|
||||
|
||||
const hasBattery = prefs.energy_sources.some(
|
||||
(source) => source.type === "battery"
|
||||
);
|
||||
const hasWater = prefs.energy_sources.some(
|
||||
(source) => source.type === "water"
|
||||
);
|
||||
@@ -74,8 +76,8 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
collection_key: "energy_dashboard",
|
||||
});
|
||||
|
||||
// Only include if we have a grid source.
|
||||
if (hasGrid) {
|
||||
// Only include if we have a grid or battery.
|
||||
if (hasGrid || hasBattery) {
|
||||
view.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_usage_graph_title"),
|
||||
type: "energy-usage-graph",
|
||||
@@ -110,8 +112,8 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have a grid.
|
||||
if (hasGrid) {
|
||||
// Only include if we have a grid or battery.
|
||||
if (hasGrid || hasBattery) {
|
||||
view.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_distribution_title"),
|
||||
type: "energy-distribution",
|
||||
@@ -120,7 +122,7 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (hasGrid || hasSolar || hasGas || hasWater) {
|
||||
if (hasGrid || hasSolar || hasGas || hasWater || hasBattery) {
|
||||
view.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_sources_table_title"
|
||||
|
@@ -10,6 +10,8 @@ import {
|
||||
addYears,
|
||||
addMonths,
|
||||
addHours,
|
||||
startOfDay,
|
||||
addDays,
|
||||
} from "date-fns";
|
||||
import type {
|
||||
BarSeriesOption,
|
||||
@@ -282,6 +284,10 @@ export function getCompareTransform(start: Date, compareStart?: Date) {
|
||||
) {
|
||||
return (ts: Date) => addMonths(ts, compareMonthDiff);
|
||||
}
|
||||
const compareDayDiff = differenceInDays(start, compareStart);
|
||||
if (compareDayDiff !== 0 && start.getTime() === startOfDay(start).getTime()) {
|
||||
return (ts: Date) => addDays(ts, compareDayDiff);
|
||||
}
|
||||
const compareOffset = start.getTime() - compareStart.getTime();
|
||||
return (ts: Date) => addMilliseconds(ts, compareOffset);
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { BarSeriesOption } from "echarts/charts";
|
||||
import type { LegendComponentOption } from "echarts/components";
|
||||
import { getGraphColorByIndex } from "../../../../common/color/colors";
|
||||
import { getEnergyColor } from "./common/color";
|
||||
import "../../../../components/ha-card";
|
||||
@@ -54,6 +55,8 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
|
||||
@state() private _data?: EnergyData;
|
||||
|
||||
@state() private _legendData?: LegendComponentOption["data"];
|
||||
|
||||
@state() private _start = startOfToday();
|
||||
|
||||
@state() private _end = endOfToday();
|
||||
@@ -185,6 +188,7 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
legend: {
|
||||
show: true,
|
||||
type: "custom",
|
||||
data: this._legendData,
|
||||
selected: this._hiddenStats.reduce((acc, stat) => {
|
||||
acc[stat] = false;
|
||||
return acc;
|
||||
@@ -310,6 +314,13 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
);
|
||||
|
||||
datasets.push(...processedData);
|
||||
this._legendData = processedData.map((d) => ({
|
||||
name: d.name as string,
|
||||
itemStyle: {
|
||||
color: d.color as string,
|
||||
borderColor: d.itemStyle?.borderColor as string,
|
||||
},
|
||||
}));
|
||||
|
||||
if (showUntracked) {
|
||||
const untrackedData = this._processUntracked(
|
||||
@@ -319,6 +330,13 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
false
|
||||
);
|
||||
datasets.push(untrackedData);
|
||||
this._legendData.push({
|
||||
name: untrackedData.name as string,
|
||||
itemStyle: {
|
||||
color: untrackedData.color as string,
|
||||
borderColor: untrackedData.itemStyle?.borderColor as string,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fillDataGapsAndRoundCaps(datasets);
|
||||
|
@@ -102,14 +102,13 @@ class HuiEnergyDistrubutionCard
|
||||
const prefs = this._data.prefs;
|
||||
const types = energySourcesByType(prefs);
|
||||
|
||||
// The strategy only includes this card if we have a grid.
|
||||
const hasConsumption = true;
|
||||
|
||||
const hasGrid =
|
||||
!!types.grid?.[0].flow_from.length || !!types.grid?.[0].flow_to.length;
|
||||
const hasSolarProduction = types.solar !== undefined;
|
||||
const hasBattery = types.battery !== undefined;
|
||||
const hasGas = types.gas !== undefined;
|
||||
const hasWater = types.water !== undefined;
|
||||
const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0;
|
||||
const hasReturnToGrid = !!types.grid?.[0].flow_to.length;
|
||||
|
||||
const { summedData, compareSummedData: _ } = getSummedData(this._data);
|
||||
const { consumption, compareConsumption: __ } = computeConsumptionData(
|
||||
@@ -163,14 +162,14 @@ class HuiEnergyDistrubutionCard
|
||||
}
|
||||
let batteryFromGrid: null | number = null;
|
||||
let batteryToGrid: null | number = null;
|
||||
if (hasBattery) {
|
||||
if (hasBattery && hasGrid) {
|
||||
batteryToGrid = consumption.total.battery_to_grid;
|
||||
batteryFromGrid = consumption.total.grid_to_battery;
|
||||
}
|
||||
|
||||
let solarToBattery: null | number = null;
|
||||
let solarToGrid: null | number = null;
|
||||
if (hasSolarProduction) {
|
||||
if (hasSolarProduction && hasGrid) {
|
||||
solarToGrid = consumption.total.solar_to_grid;
|
||||
}
|
||||
if (hasSolarProduction && hasBattery) {
|
||||
@@ -182,7 +181,9 @@ class HuiEnergyDistrubutionCard
|
||||
batteryConsumption = Math.max(consumption.total.used_battery, 0);
|
||||
}
|
||||
|
||||
const gridConsumption = Math.max(consumption.total.used_grid, 0);
|
||||
const gridConsumption = hasGrid
|
||||
? Math.max(consumption.total.used_grid, 0)
|
||||
: 0;
|
||||
|
||||
const totalHomeConsumption = Math.max(0, consumption.total.used_total);
|
||||
|
||||
@@ -206,7 +207,11 @@ class HuiEnergyDistrubutionCard
|
||||
// This fallback is used in the demo
|
||||
let electricityMapUrl = "https://app.electricitymap.org";
|
||||
|
||||
if (this._data.co2SignalEntity && this._data.fossilEnergyConsumption) {
|
||||
if (
|
||||
hasGrid &&
|
||||
this._data.co2SignalEntity &&
|
||||
this._data.fossilEnergyConsumption
|
||||
) {
|
||||
// Calculate high carbon consumption
|
||||
const highCarbonEnergy = Object.values(
|
||||
this._data.fossilEnergyConsumption
|
||||
@@ -225,7 +230,7 @@ class HuiEnergyDistrubutionCard
|
||||
if (gridConsumption !== totalFromGrid) {
|
||||
// Only get the part that was used for consumption and not the battery
|
||||
highCarbonConsumption =
|
||||
highCarbonEnergy * (gridConsumption / totalFromGrid);
|
||||
highCarbonEnergy * (gridConsumption! / totalFromGrid);
|
||||
} else {
|
||||
highCarbonConsumption = highCarbonEnergy;
|
||||
}
|
||||
@@ -378,41 +383,43 @@ class HuiEnergyDistrubutionCard
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="row">
|
||||
<div class="circle-container grid">
|
||||
<div class="circle">
|
||||
<ha-svg-icon .path=${mdiTransmissionTower}></ha-svg-icon>
|
||||
${returnedToGrid !== null
|
||||
? html`<span class="return">
|
||||
<ha-svg-icon
|
||||
class="small"
|
||||
.path=${mdiArrowLeft}
|
||||
></ha-svg-icon
|
||||
>${formatConsumptionShort(
|
||||
${hasGrid
|
||||
? html`<div class="circle-container grid">
|
||||
<div class="circle">
|
||||
<ha-svg-icon .path=${mdiTransmissionTower}></ha-svg-icon>
|
||||
${returnedToGrid !== null
|
||||
? html`<span class="return">
|
||||
<ha-svg-icon
|
||||
class="small"
|
||||
.path=${mdiArrowLeft}
|
||||
></ha-svg-icon
|
||||
>${formatConsumptionShort(
|
||||
this.hass,
|
||||
returnedToGrid,
|
||||
"kWh"
|
||||
)}
|
||||
</span>`
|
||||
: ""}
|
||||
<span class="consumption">
|
||||
${hasReturnToGrid
|
||||
? html`<ha-svg-icon
|
||||
class="small"
|
||||
.path=${mdiArrowRight}
|
||||
></ha-svg-icon>`
|
||||
: ""}${formatConsumptionShort(
|
||||
this.hass,
|
||||
returnedToGrid,
|
||||
totalFromGrid,
|
||||
"kWh"
|
||||
)}
|
||||
</span>`
|
||||
: ""}
|
||||
<span class="consumption">
|
||||
${hasReturnToGrid
|
||||
? html`<ha-svg-icon
|
||||
class="small"
|
||||
.path=${mdiArrowRight}
|
||||
></ha-svg-icon>`
|
||||
: ""}${formatConsumptionShort(
|
||||
this.hass,
|
||||
totalFromGrid,
|
||||
"kWh"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<span class="label"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.grid"
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<span class="label"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.grid"
|
||||
)}</span
|
||||
>
|
||||
</div> `
|
||||
: html`<div class="grid-spacer"></div>`}
|
||||
<div class="circle-container home">
|
||||
<div
|
||||
class="circle ${classMap({
|
||||
@@ -480,22 +487,27 @@ class HuiEnergyDistrubutionCard
|
||||
shape-rendering="geometricPrecision"
|
||||
/>`
|
||||
: ""}
|
||||
<circle
|
||||
${hasGrid
|
||||
? svg`<circle
|
||||
class="grid"
|
||||
cx="40"
|
||||
cy="40"
|
||||
r="38"
|
||||
stroke-dasharray="${homeHighCarbonCircumference ??
|
||||
CIRCLE_CIRCUMFERENCE -
|
||||
homeSolarCircumference! -
|
||||
(homeBatteryCircumference ||
|
||||
0)} ${homeHighCarbonCircumference !== undefined
|
||||
? CIRCLE_CIRCUMFERENCE - homeHighCarbonCircumference
|
||||
: homeSolarCircumference! +
|
||||
(homeBatteryCircumference || 0)}"
|
||||
stroke-dasharray="${
|
||||
homeHighCarbonCircumference ??
|
||||
CIRCLE_CIRCUMFERENCE -
|
||||
homeSolarCircumference! -
|
||||
(homeBatteryCircumference || 0)
|
||||
} ${
|
||||
homeHighCarbonCircumference !== undefined
|
||||
? CIRCLE_CIRCUMFERENCE - homeHighCarbonCircumference
|
||||
: homeSolarCircumference! +
|
||||
(homeBatteryCircumference || 0)
|
||||
}"
|
||||
stroke-dashoffset="0"
|
||||
shape-rendering="geometricPrecision"
|
||||
/>
|
||||
/>`
|
||||
: nothing}
|
||||
</svg>`
|
||||
: ""}
|
||||
</div>
|
||||
@@ -619,15 +631,19 @@ class HuiEnergyDistrubutionCard
|
||||
d="M55,100 v-15 c0,-35 10,-30 30,-30 h20"
|
||||
vector-effect="non-scaling-stroke"
|
||||
></path>
|
||||
<path
|
||||
id="battery-grid"
|
||||
class=${classMap({
|
||||
"battery-from-grid": Boolean(batteryFromGrid),
|
||||
"battery-to-grid": Boolean(batteryToGrid),
|
||||
})}
|
||||
d="M45,100 v-15 c0,-35 -10,-30 -30,-30 h-20"
|
||||
vector-effect="non-scaling-stroke"
|
||||
></path>
|
||||
${
|
||||
hasGrid
|
||||
? svg`<path
|
||||
id="battery-grid"
|
||||
class=${classMap({
|
||||
"battery-from-grid": Boolean(batteryFromGrid),
|
||||
"battery-to-grid": Boolean(batteryToGrid),
|
||||
})}
|
||||
d="M45,100 v-15 c0,-35 -10,-30 -30,-30 h-20"
|
||||
vector-effect="non-scaling-stroke"
|
||||
></path>`
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
: ""}
|
||||
${hasBattery && hasSolarProduction
|
||||
@@ -638,12 +654,14 @@ class HuiEnergyDistrubutionCard
|
||||
vector-effect="non-scaling-stroke"
|
||||
></path>`
|
||||
: ""}
|
||||
<path
|
||||
class="grid"
|
||||
id="grid"
|
||||
d="M0,${hasBattery ? 50 : hasSolarProduction ? 56 : 53} H100"
|
||||
vector-effect="non-scaling-stroke"
|
||||
></path>
|
||||
${hasGrid
|
||||
? svg`<path
|
||||
class="grid"
|
||||
id="grid"
|
||||
d="M0,${hasBattery ? 50 : hasSolarProduction ? 56 : 53} H100"
|
||||
vector-effect="non-scaling-stroke"
|
||||
></path>`
|
||||
: nothing}
|
||||
${solarToGrid && this._animate
|
||||
? svg`<circle
|
||||
r="1"
|
||||
@@ -839,6 +857,10 @@ class HuiEnergyDistrubutionCard
|
||||
.spacer {
|
||||
width: 84px;
|
||||
}
|
||||
.grid-spacer {
|
||||
width: 84px;
|
||||
height: 100px;
|
||||
}
|
||||
.circle {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
|
@@ -418,12 +418,11 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
|
||||
|
||||
.keypad {
|
||||
--keypad-columns: 3;
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--keypad-columns), auto);
|
||||
grid-auto-rows: auto;
|
||||
grid-gap: 24px;
|
||||
grid-gap: 16px;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
@@ -384,15 +384,22 @@ export class HuiAreaCard
|
||||
areaSensorEntityId = area.humidity_entity_id;
|
||||
break;
|
||||
}
|
||||
const areaEntity = areaSensorEntityId
|
||||
? this.hass.states[areaSensorEntityId]
|
||||
: undefined;
|
||||
const areaEntity =
|
||||
areaSensorEntityId &&
|
||||
this.hass.states[areaSensorEntityId] &&
|
||||
!isUnavailableState(this.hass.states[areaSensorEntityId].state)
|
||||
? this.hass.states[areaSensorEntityId]
|
||||
: undefined;
|
||||
if (
|
||||
areaEntity ||
|
||||
entitiesByDomain[domain].some(
|
||||
(entity) => entity.attributes.device_class === deviceClass
|
||||
)
|
||||
) {
|
||||
let value = areaEntity
|
||||
? this.hass.formatEntityState(areaEntity)
|
||||
: this._average(domain, deviceClass);
|
||||
if (!value) value = "—";
|
||||
sensors.push(html`
|
||||
<div class="sensor">
|
||||
<ha-domain-icon
|
||||
@@ -400,9 +407,7 @@ export class HuiAreaCard
|
||||
.domain=${domain}
|
||||
.deviceClass=${deviceClass}
|
||||
></ha-domain-icon>
|
||||
${areaEntity
|
||||
? this.hass.formatEntityState(areaEntity)
|
||||
: this._average(domain, deviceClass)}
|
||||
${value}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
@@ -224,19 +224,19 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
filter: colored ? stateColorBrightness(stateObj) : undefined,
|
||||
height: this._config.icon_height
|
||||
? this._config.icon_height
|
||||
: "",
|
||||
: undefined,
|
||||
})}
|
||||
></ha-state-icon>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
${this._config.show_name
|
||||
? html`<span tabindex="-1" .title=${name}>${name}</span>`
|
||||
: ""}
|
||||
: nothing}
|
||||
${this._config.show_state && stateObj
|
||||
? html`<span class="state">
|
||||
${this.hass.formatEntityState(stateObj)}
|
||||
</span>`
|
||||
: ""}
|
||||
: nothing}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
@@ -282,7 +282,8 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 4% 0;
|
||||
font-size: 16.8px;
|
||||
font-size: var(--ha-font-size-l);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
|
@@ -12,7 +12,8 @@ import {
|
||||
attachConditionMediaQueriesListeners,
|
||||
checkConditionsMet,
|
||||
} from "../common/validate-condition";
|
||||
import { createCardElement } from "../create-element/create-card-element";
|
||||
import { tryCreateCardElement } from "../create-element/create-card-element";
|
||||
import { createErrorCardElement } from "../create-element/create-element-base";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../types";
|
||||
|
||||
declare global {
|
||||
@@ -71,10 +72,23 @@ export class HuiCard extends ReactiveElement {
|
||||
public getGridOptions(): LovelaceGridOptions {
|
||||
const elementOptions = this.getElementGridOptions();
|
||||
const configOptions = this.getConfigGridOptions();
|
||||
return {
|
||||
const mergedConfig = {
|
||||
...elementOptions,
|
||||
...configOptions,
|
||||
};
|
||||
|
||||
// If the element has fixed rows or columns, we use the values from the element
|
||||
if (elementOptions.fixed_rows) {
|
||||
mergedConfig.rows = elementOptions.rows;
|
||||
delete mergedConfig.min_rows;
|
||||
delete mergedConfig.max_rows;
|
||||
}
|
||||
if (elementOptions.fixed_columns) {
|
||||
mergedConfig.columns = elementOptions.columns;
|
||||
delete mergedConfig.min_columns;
|
||||
delete mergedConfig.max_columns;
|
||||
}
|
||||
return mergedConfig;
|
||||
}
|
||||
|
||||
// options provided by the element
|
||||
@@ -82,7 +96,9 @@ export class HuiCard extends ReactiveElement {
|
||||
if (!this._element) return {};
|
||||
|
||||
if (this._element.getGridOptions) {
|
||||
return this._element.getGridOptions();
|
||||
const options = this._element.getGridOptions();
|
||||
// Some custom cards might return undefined, so we ensure we return an object
|
||||
return options || {};
|
||||
}
|
||||
if (this._element.getLayoutOptions) {
|
||||
// Disabled for now to avoid spamming the console, need to be re-enabled when hui-card performance are fixed
|
||||
@@ -119,7 +135,15 @@ export class HuiCard extends ReactiveElement {
|
||||
}
|
||||
|
||||
private _loadElement(config: LovelaceCardConfig) {
|
||||
this._element = createCardElement(config);
|
||||
try {
|
||||
this._element = tryCreateCardElement(config);
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : undefined;
|
||||
this._element = createErrorCardElement({
|
||||
type: "error",
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
this._elementConfig = config;
|
||||
if (this.hass) {
|
||||
this._element.hass = this.hass;
|
||||
@@ -200,6 +224,7 @@ export class HuiCard extends ReactiveElement {
|
||||
this._element.preview = this.preview;
|
||||
// For backwards compatibility
|
||||
(this._element as any).editMode = this.preview;
|
||||
fireEvent(this, "card-updated");
|
||||
} catch (e: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(this.config?.type, e);
|
||||
|
@@ -301,7 +301,7 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
|
||||
.value {
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { mdiAlertCircleOutline, mdiAlertOutline } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { mdiAlertCircleOutline, mdiAlertOutline } from "@mdi/js";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../types";
|
||||
import type { ErrorCardConfig } from "./types";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-svg-icon";
|
||||
|
||||
const ERROR_ICONS = {
|
||||
warning: mdiAlertOutline,
|
||||
@@ -30,9 +30,10 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
|
||||
public getGridOptions(): LovelaceGridOptions {
|
||||
return {
|
||||
columns: 6,
|
||||
rows: 1,
|
||||
rows: this.preview ? "auto" : 1,
|
||||
min_rows: 1,
|
||||
min_columns: 6,
|
||||
fixed_rows: this.preview,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,17 +46,24 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
|
||||
const error =
|
||||
this._config?.error ||
|
||||
this.hass?.localize("ui.errors.config.configuration_error");
|
||||
const showTitle = this.hass === undefined || this.hass?.user?.is_admin;
|
||||
const showTitle =
|
||||
this.hass === undefined || this.hass?.user?.is_admin || this.preview;
|
||||
const showMessage = this.preview;
|
||||
|
||||
return html`
|
||||
<ha-card class="${this.severity} ${showTitle ? "" : "no-title"}">
|
||||
<div class="icon">
|
||||
<slot name="icon">
|
||||
<ha-svg-icon .path=${ERROR_ICONS[this.severity]}></ha-svg-icon>
|
||||
</slot>
|
||||
<div class="header">
|
||||
<div class="icon">
|
||||
<slot name="icon">
|
||||
<ha-svg-icon .path=${ERROR_ICONS[this.severity]}></ha-svg-icon>
|
||||
</slot>
|
||||
</div>
|
||||
${showTitle
|
||||
? html`<div class="title"><slot>${error}</slot></div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${showTitle
|
||||
? html`<div class="title"><slot>${error}</slot></div>`
|
||||
${showMessage && this._config?.message
|
||||
? html`<div class="message">${this._config.message}</div>`
|
||||
: nothing}
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -65,10 +73,6 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
|
||||
ha-card {
|
||||
height: 100%;
|
||||
border-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
ha-card::after {
|
||||
position: absolute;
|
||||
@@ -81,6 +85,15 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
|
||||
content: "";
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
.message {
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
.no-title {
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -90,13 +103,13 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
|
||||
text-overflow: ellipsis;
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
}
|
||||
ha-card.warning > .icon {
|
||||
ha-card.warning .icon {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
ha-card.warning::after {
|
||||
background-color: var(--warning-color);
|
||||
}
|
||||
ha-card.error > .icon {
|
||||
ha-card.error .icon {
|
||||
color: var(--error-color);
|
||||
}
|
||||
ha-card.error::after {
|
||||
|
@@ -45,6 +45,7 @@ export const DEFAULT_ZOOM = 14;
|
||||
interface MapEntityConfig extends EntityConfig {
|
||||
label_mode?: "state" | "attribute" | "name";
|
||||
attribute?: string;
|
||||
unit?: string;
|
||||
focus?: boolean;
|
||||
}
|
||||
|
||||
@@ -52,6 +53,7 @@ interface GeoEntity {
|
||||
entity_id: string;
|
||||
label_mode?: "state" | "attribute" | "name" | "icon";
|
||||
attribute?: string;
|
||||
unit?: string;
|
||||
focus: boolean;
|
||||
}
|
||||
|
||||
@@ -430,6 +432,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
entity_id: stateObj.entity_id,
|
||||
label_mode: sourceObj?.label_mode ?? allSource?.label_mode,
|
||||
attribute: sourceObj?.attribute ?? allSource?.attribute,
|
||||
unit: sourceObj?.unit ?? allSource?.unit,
|
||||
focus: sourceObj
|
||||
? (sourceObj.focus ?? true)
|
||||
: (allSource?.focus ?? true),
|
||||
@@ -446,6 +449,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
color: this._getColor(entityConf.entity),
|
||||
label_mode: entityConf.label_mode,
|
||||
attribute: entityConf.attribute,
|
||||
unit: entityConf.unit,
|
||||
focus: entityConf.focus,
|
||||
name: entityConf.name,
|
||||
})),
|
||||
|
@@ -323,6 +323,7 @@ interface GeoLocationSourceConfig {
|
||||
source: string;
|
||||
label_mode?: "name" | "state" | "attribute" | "icon";
|
||||
attribute?: string;
|
||||
unit?: string;
|
||||
focus?: boolean;
|
||||
}
|
||||
|
||||
|
@@ -85,7 +85,7 @@ export class HuiBadgeEditMode extends LitElement {
|
||||
if (this._touchStarted) return;
|
||||
this._hover = true;
|
||||
});
|
||||
this.addEventListener("mouseout", () => {
|
||||
this.addEventListener("mouseleave", () => {
|
||||
this._hover = false;
|
||||
});
|
||||
this.addEventListener("click", () => {
|
||||
|
@@ -71,7 +71,7 @@ export class HuiCardEditMode extends LitElement {
|
||||
if (this._touchStarted) return;
|
||||
this._hover = true;
|
||||
});
|
||||
this.addEventListener("mouseout", () => {
|
||||
this.addEventListener("mouseleave", () => {
|
||||
this._hover = false;
|
||||
});
|
||||
this.addEventListener("click", () => {
|
||||
|
@@ -297,24 +297,17 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _dateRangeChanged(ev) {
|
||||
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
|
||||
this._startDate = calcDate(
|
||||
ev.detail.value.startDate,
|
||||
startOfDay,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
{
|
||||
weekStartsOn,
|
||||
}
|
||||
this.hass.config
|
||||
);
|
||||
this._endDate = calcDate(
|
||||
ev.detail.value.endDate,
|
||||
endOfDay,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
{
|
||||
weekStartsOn,
|
||||
}
|
||||
this.hass.config
|
||||
);
|
||||
|
||||
this._updateCollectionPeriod();
|
||||
|
@@ -40,6 +40,7 @@ export const mapEntitiesConfigStruct = union([
|
||||
entity: string(),
|
||||
label_mode: optional(string()),
|
||||
attribute: optional(string()),
|
||||
unit: optional(string()),
|
||||
focus: optional(boolean()),
|
||||
name: optional(string()),
|
||||
}),
|
||||
@@ -51,6 +52,7 @@ const geoSourcesConfigStruct = union([
|
||||
source: string(),
|
||||
label_mode: optional(string()),
|
||||
attribute: optional(string()),
|
||||
unit: optional(string()),
|
||||
focus: optional(boolean()),
|
||||
}),
|
||||
string(),
|
||||
|
@@ -57,6 +57,7 @@ import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../dialogs/generic/show-dialog-box";
|
||||
import { showMoreInfoDialog } from "../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import {
|
||||
QuickBarMode,
|
||||
showQuickBar,
|
||||
@@ -75,9 +76,9 @@ import { getLovelaceStrategy } from "./strategies/get-strategy";
|
||||
import { isLegacyStrategyConfig } from "./strategies/legacy-strategy";
|
||||
import type { Lovelace } from "./types";
|
||||
import "./views/hui-view";
|
||||
import "./views/hui-view-container";
|
||||
import type { HUIView } from "./views/hui-view";
|
||||
import "./views/hui-view-background";
|
||||
import "./views/hui-view-container";
|
||||
|
||||
@customElement("hui-root")
|
||||
class HUIRoot extends LitElement {
|
||||
@@ -490,7 +491,16 @@ class HUIRoot extends LitElement {
|
||||
} else if (searchParams.conversation === "1") {
|
||||
this._clearParam("conversation");
|
||||
this._showVoiceCommandDialog();
|
||||
} else if (searchParams["more-info-entity-id"]) {
|
||||
const entityId = searchParams["more-info-entity-id"];
|
||||
this._clearParam("more-info-entity-id");
|
||||
// Wait for the next render to ensure the view is fully loaded
|
||||
// because the more info dialog is closed when the url changes
|
||||
afterNextRender(() => {
|
||||
this._showMoreInfoDialog(entityId);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", this._handleWindowScroll, {
|
||||
passive: true,
|
||||
});
|
||||
@@ -730,6 +740,10 @@ class HUIRoot extends LitElement {
|
||||
showVoiceCommandDialog(this, this.hass, { pipeline_id: "last_used" });
|
||||
}
|
||||
|
||||
private _showMoreInfoDialog(entityId: string): void {
|
||||
showMoreInfoDialog(this, { entityId });
|
||||
}
|
||||
|
||||
private _handleEnableEditMode(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
@@ -1109,6 +1123,16 @@ class HUIRoot extends LitElement {
|
||||
sl-tab[aria-selected="true"] .edit-icon {
|
||||
display: inline-flex;
|
||||
}
|
||||
sl-tab::part(base) {
|
||||
padding-inline-start: var(
|
||||
--ha-tab-padding-start,
|
||||
var(--sl-spacing-large)
|
||||
);
|
||||
padding-inline-end: var(
|
||||
--ha-tab-padding-end,
|
||||
var(--sl-spacing-large)
|
||||
);
|
||||
}
|
||||
sl-tab::part(base) {
|
||||
padding-top: calc((var(--header-height) - 20px) / 2);
|
||||
padding-bottom: calc((var(--header-height) - 20px) / 2 - 2px);
|
||||
|
@@ -62,6 +62,8 @@ export interface LovelaceGridOptions {
|
||||
min_columns?: number;
|
||||
min_rows?: number;
|
||||
max_rows?: number;
|
||||
fixed_rows?: boolean;
|
||||
fixed_columns?: boolean;
|
||||
}
|
||||
|
||||
export interface LovelaceCard extends HTMLElement {
|
||||
|
@@ -873,7 +873,8 @@
|
||||
}
|
||||
},
|
||||
"service-picker": {
|
||||
"action": "Action"
|
||||
"action": "Action",
|
||||
"no_match": "No matching actions found"
|
||||
},
|
||||
"service-control": {
|
||||
"required": "This field is required",
|
||||
@@ -901,6 +902,8 @@
|
||||
"hidden": "{number} hidden",
|
||||
"clear": "Clear",
|
||||
"ungrouped": "Ungrouped",
|
||||
"collapse": "Collapse",
|
||||
"expand": "Expand",
|
||||
"settings": {
|
||||
"header": "Customize",
|
||||
"hide": "Hide column {title}",
|
||||
@@ -1578,7 +1581,7 @@
|
||||
"upload_in_progress": "A backup upload is currently in progress. The action will automatically proceed once the upload process is complete.",
|
||||
"restore_in_progress": "A backup restore is currently in progress. The action will automatically proceed once the restore process is complete.",
|
||||
"wait_for_backup": "Wait for the backup creation to finish",
|
||||
"error_backup_state": "An error occured while getting the current backup state. Error: {error}",
|
||||
"error_backup_state": "An error occurred while getting the current backup state. Error: {error}",
|
||||
"wait_for_upload": "Wait for backup upload to finish",
|
||||
"wait_for_restore": "Wait for backup restore to finish",
|
||||
"reload": {
|
||||
@@ -1781,7 +1784,7 @@
|
||||
"buttons": {
|
||||
"add": "Add devices via this device",
|
||||
"remove": "Remove",
|
||||
"manage": "Manage zigbee device",
|
||||
"manage": "Manage Zigbee device",
|
||||
"reconfigure": "Reconfigure",
|
||||
"view_network": "View network"
|
||||
},
|
||||
@@ -2377,7 +2380,7 @@
|
||||
},
|
||||
"generate": {
|
||||
"sync": {
|
||||
"title": "Synchonization",
|
||||
"title": "Synchronization",
|
||||
"name": "Backup name",
|
||||
"locations": "Locations",
|
||||
"locations_description": "What locations you want to automatically backup to.",
|
||||
@@ -5131,7 +5134,7 @@
|
||||
"restore_entity_id_selected": {
|
||||
"button": "Recreate entity IDs of selected",
|
||||
"confirm_title": "Recreate entity IDs?",
|
||||
"confirm_text": "Are you sure you want to change the entity IDs of these entities? You will have to change you dashboards, automations and scripts to use the new entity IDs.",
|
||||
"confirm_text": "Are you sure you want to change the entity IDs of these entities? You will have to change your dashboards, automations and scripts to use the new entity IDs.",
|
||||
"changes": "The following entity IDs will be updated:"
|
||||
},
|
||||
"delete_selected": {
|
||||
@@ -5804,8 +5807,8 @@
|
||||
"provisioned_devices": "Provisioned devices",
|
||||
"not_ready": "{count} not ready",
|
||||
"nvm_backup": {
|
||||
"title": "Backup and Restore",
|
||||
"description": "Back up or restore your Z-Wave controller's Non-Volatile Memory (NVM). The NVM contains your network information including paired devices. It's recommended to create a backup before making any major changes to your Z-Wave network.",
|
||||
"title": "Backup and restore",
|
||||
"description": "Back up or restore your Z-Wave controller's non-volatile memory (NVM). The NVM contains your network information including paired devices. It's recommended to create a backup before making any major changes to your Z-Wave network.",
|
||||
"download_backup": "Download backup",
|
||||
"restore_backup": "Restore from backup",
|
||||
"backup_failed": "Failed to download backup",
|
||||
@@ -6010,9 +6013,9 @@
|
||||
"default": "Default"
|
||||
},
|
||||
"network_status": {
|
||||
"connected": "Connected",
|
||||
"connecting": "Connecting",
|
||||
"unknown": "Unknown"
|
||||
"connected": "status: connected",
|
||||
"connecting": "status: connecting",
|
||||
"unknown": "status: unknown"
|
||||
},
|
||||
"add_node": {
|
||||
"title": "Add a Z-Wave device",
|
||||
|
Reference in New Issue
Block a user