Compare commits

..

1 Commits

Author SHA1 Message Date
Petar Petrov
eefbfd98b3 Update energy usage calculation to fix remaining tests 2025-04-30 08:44:04 +03:00
129 changed files with 2452 additions and 4039 deletions

View File

@@ -26,7 +26,7 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.27.1",
"@babel/runtime": "7.27.0",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/commands": "6.8.1",
@@ -34,7 +34,7 @@
"@codemirror/legacy-modes": "6.5.1",
"@codemirror/search": "6.5.10",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.36.7",
"@codemirror/view": "6.36.6",
"@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.5",
"@vaadin/vaadin-themable-mixin": "24.7.5",
"@vaadin/combo-box": "24.7.4",
"@vaadin/vaadin-themable-mixin": "24.7.4",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
@@ -99,7 +99,7 @@
"barcode-detector": "3.0.1",
"color-name": "2.0.0",
"comlink": "4.4.2",
"core-js": "3.42.0",
"core-js": "3.41.0",
"cropperjs": "1.6.2",
"date-fns": "4.1.0",
"date-fns-tz": "3.2.0",
@@ -131,7 +131,7 @@
"qrcode": "1.5.4",
"roboto-fontface": "0.10.0",
"rrule": "2.8.1",
"sortablejs": "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch",
"sortablejs": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch",
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "3.0.0",
@@ -150,18 +150,18 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.27.1",
"@babel/core": "7.26.10",
"@babel/helper-define-polyfill-provider": "0.6.4",
"@babel/plugin-transform-runtime": "7.27.1",
"@babel/preset-env": "7.27.1",
"@bundle-stats/plugin-webpack-filter": "4.20.0",
"@lokalise/node-api": "14.5.0",
"@babel/plugin-transform-runtime": "7.26.10",
"@babel/preset-env": "7.26.9",
"@bundle-stats/plugin-webpack-filter": "4.19.1",
"@lokalise/node-api": "14.4.0",
"@octokit/auth-oauth-device": "7.1.5",
"@octokit/plugin-retry": "7.2.1",
"@octokit/rest": "21.1.1",
"@rsdoctor/rspack-plugin": "1.0.2",
"@rspack/cli": "1.3.8",
"@rspack/core": "1.3.8",
"@rspack/cli": "1.3.7",
"@rspack/core": "1.3.7",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.21",
"@types/chromecast-caf-sender": "1.0.11",
@@ -185,7 +185,7 @@
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.0",
"eslint": "9.26.0",
"eslint": "9.25.1",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.1.2",
"eslint-import-resolver-webpack": "0.13.10",
@@ -193,7 +193,7 @@
"eslint-plugin-lit": "2.1.1",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "3.0.1",
"eslint-plugin-wc": "3.0.0",
"fancy-log": "2.0.0",
"fs-extra": "11.3.0",
"glob": "11.0.2",
@@ -219,7 +219,7 @@
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.8.3",
"typescript-eslint": "8.31.1",
"typescript-eslint": "8.31.0",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.1.2",
"webpack-stats-plugin": "1.1.3",

View File

@@ -1,13 +1,13 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8284 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 37.9999L37.5 39.4999L76.9105 39.4999V37.9999V36.4999L37.5 36.4999L37.5 37.9999Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8283 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 39.4999L76.9105 39.4999V36.4999L37.5 36.4999L37.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M30.8239 22.3365L38.8239 38.8365L30.3239 50.3365" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
<mask id="mask0_2_779" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
<mask id="mask0_1110_23734" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
<path d="M45.75 42.075C45.75 42.4462 45.4462 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_2_779)">
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
<g mask="url(#mask0_1110_23734)">
<rect x="30" y="27" width="18" height="18" fill="#212121"/>
</g>
<path d="M85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999C82 36.343 83.3431 34.9999 85 34.9999Z" stroke="#00AFFF" stroke-width="2"/>
<path d="M82 37.9999C82 36.343 83.3431 34.9999 85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
<rect x="23" y="11" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
<rect x="22" y="52" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,19 +1,19 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M55.1358 38.5084C55.9608 38.4334 56.5688 37.7038 56.4938 36.8788C56.4188 36.0538 55.6892 35.4457 54.8642 35.5207L55.1358 38.5084ZM38.5 38.5146L38.6358 40.0084L55.1358 38.5084L55 37.0146L54.8642 35.5207L38.3642 37.0207L38.5 38.5146Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M30.5 22L37.9722 37.4115C38.2967 38.0807 38.223 38.8747 37.781 39.4728L30 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<circle cx="39" cy="36" r="34" fill="white"/>
<circle cx="39" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
<path d="M33.8777 12.5216C35.4905 12.1798 37.1631 12 38.8777 12C50.2401 12 59.7582 19.8959 62.2445 30.5M32 59C34.1788 59.6506 36.4874 60 38.8777 60C48.9498 60 57.5728 53.7955 61.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M30.5 22L37.9722 37.4115C38.2967 38.0807 38.223 38.8747 37.781 39.4728L30 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<mask id="mask0_2_810" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
<path d="M45.75 42.075C45.75 42.4462 45.4463 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
<path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<circle cx="47" cy="36" r="34" fill="white"/>
<circle cx="47" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
<path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<mask id="mask0_1110_23775" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18">
<path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_2_810)">
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
<g mask="url(#mask0_1110_23775)">
<rect x="38" y="27" width="18" height="18" fill="#212121"/>
</g>
<path d="M55.5 39.4999C56.3284 39.4999 57 38.8283 57 37.9999C57 37.1715 56.3284 36.4999 55.5 36.4999L55.5 39.4999ZM41.5 37.9999L41.5 39.4999L55.5 39.4999L55.5 37.9999L55.5 36.4999L41.5 36.4999L41.5 37.9999Z" fill="#00AFFF" fill-opacity="0.3"/>
<rect x="23" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<rect x="22" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M63 34.9999C64.6569 34.9999 66 36.343 66 37.9999C66 39.6567 64.6569 40.9999 63 40.9999C61.3431 40.9999 60 39.6567 60 37.9999C60 36.343 61.3431 34.9999 63 34.9999Z" stroke="#00AFFF" stroke-width="2"/>
<path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1,18 +1,19 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M55.1358 38.5084C55.9608 38.4334 56.5688 37.7038 56.4938 36.8788C56.4188 36.0538 55.6892 35.4457 54.8642 35.5207L55.1358 38.5084ZM38.5 38.5146L38.6358 40.0084L55.1358 38.5084L55 37.0146L54.8642 35.5207L38.3642 37.0207L38.5 38.5146Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M30.5 22L37.9722 37.4115C38.2967 38.0807 38.223 38.8747 37.781 39.4728L30 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<circle cx="39" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
<path d="M33.8777 12.5216C35.4905 12.1798 37.1631 12 38.8777 12C50.2401 12 59.7582 19.8959 62.2445 30.5M32 59C34.1788 59.6506 36.4874 60 38.8777 60C48.9498 60 57.5728 53.7955 61.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M30.5 22L37.9722 37.4115C38.2967 38.0807 38.223 38.8747 37.781 39.4728L30 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<mask id="mask0_2_810" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
<path d="M45.75 42.075C45.75 42.4462 45.4463 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
<path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<circle cx="47" cy="36" r="34" fill="#1C1C1C"/>
<circle cx="47" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
<path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<mask id="mask0_1180_4965" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18">
<path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_2_810)">
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
<g mask="url(#mask0_1180_4965)">
<rect x="38" y="27" width="18" height="18" fill="#00AFFF"/>
</g>
<path d="M55.5 39.4999C56.3284 39.4999 57 38.8283 57 37.9999C57 37.1715 56.3284 36.4999 55.5 36.4999L55.5 39.4999ZM41.5 37.9999L41.5 39.4999L55.5 39.4999L55.5 37.9999L55.5 36.4999L41.5 36.4999L41.5 37.9999Z" fill="#00AFFF" fill-opacity="0.3"/>
<rect x="23" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<rect x="22" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M63 34.9999C64.6569 34.9999 66 36.343 66 37.9999C66 39.6567 64.6569 40.9999 63 40.9999C61.3431 40.9999 60 39.6567 60 37.9999C60 36.343 61.3431 34.9999 63 34.9999Z" stroke="#00AFFF" stroke-width="2"/>
<path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20250430.0"
version = "20250326.0"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"

View File

@@ -9,8 +9,6 @@ import type { LitElement } from "lit";
*/
export interface DragScrollControllerConfig {
selector: string;
enabled?: boolean;
trackScroll?: boolean;
}
export class DragScrollController implements ReactiveController {
@@ -24,109 +22,38 @@ export class DragScrollController implements ReactiveController {
public scrollLeft = 0;
public scrolledStart = false;
public scrolledEnd = false;
private _host: ReactiveControllerHost & LitElement;
private _selector: string;
private _scrollContainer?: HTMLElement | null;
private _enabled = true;
private _trackScroll = false;
public get enabled(): boolean {
return this._enabled;
}
public set enabled(value: boolean) {
if (value === this._enabled) {
return;
}
this._enabled = value;
if (this._enabled) {
this._attach();
} else {
this._detach();
}
this._host.requestUpdate();
}
constructor(
host: ReactiveControllerHost & LitElement,
{ selector, enabled, trackScroll }: DragScrollControllerConfig
{ selector }: DragScrollControllerConfig
) {
this._selector = selector;
this._host = host;
this._trackScroll = trackScroll ?? false;
this.enabled = enabled ?? true;
host.addController(this);
}
hostUpdated() {
if (!this.enabled || this._scrollContainer) {
if (this._scrollContainer) {
return;
}
this._attach();
}
hostDisconnected() {
this._detach();
}
private _attach() {
this._scrollContainer = this._host.renderRoot?.querySelector(
this._selector
);
if (this._scrollContainer) {
this._scrollContainer.addEventListener("mousedown", this._mouseDown);
if (this._trackScroll) {
this._scrollContainer.addEventListener("scroll", this._onScroll);
this.scrolledStart = this._scrollContainer.scrollLeft > 0;
this.scrolledEnd =
this._scrollContainer.scrollLeft + this._scrollContainer.offsetWidth <
this._scrollContainer.scrollWidth;
this._host.requestUpdate();
}
}
}
private _detach() {
hostDisconnected() {
window.removeEventListener("mousemove", this._mouseMove);
window.removeEventListener("mouseup", this._mouseUp);
if (this._scrollContainer) {
this._scrollContainer.removeEventListener("mousedown", this._mouseDown);
this._scrollContainer.removeEventListener("scroll", this._onScroll);
this._scrollContainer = undefined;
}
this.scrolled = false;
this.scrolling = false;
this.scrolledStart = false;
this.scrolledEnd = false;
this.mouseIsDown = false;
this.scrollStartX = 0;
this.scrollLeft = 0;
}
private _onScroll = (event: Event) => {
const oldScrolledStart = this.scrolledStart;
const oldScrolledEnd = this.scrolledEnd;
const container = event.currentTarget as HTMLElement;
this.scrolledStart = container.scrollLeft > 0;
this.scrolledEnd =
container.scrollLeft + container.offsetWidth < container.scrollWidth;
if (
this.scrolledStart !== oldScrolledStart ||
this.scrolledEnd !== oldScrolledEnd
) {
this._host.requestUpdate();
}
};
private _mouseDown = (event: MouseEvent) => {
const scrollContainer = this._scrollContainer;

View File

@@ -1,5 +1,6 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { ReactiveElement } from "lit";
import { ReactiveElement } from "lit";
import type { InternalPropertyDeclaration } from "lit/decorators";
type Callback = (oldValue: any, newValue: any) => void;
@@ -107,6 +108,7 @@ export function storage(options: {
storage?: "localStorage" | "sessionStorage";
subscribe?: boolean;
state?: boolean;
stateOptions?: InternalPropertyDeclaration;
serializer?: (value: any) => any;
deserializer?: (value: any) => any;
}) {
@@ -172,7 +174,7 @@ export function storage(options: {
performUpdate.call(this);
};
if (options.subscribe) {
if (options.state && options.subscribe) {
const connectedCallback = proto.connectedCallback;
const disconnectedCallback = proto.disconnectedCallback;
@@ -190,6 +192,12 @@ export function storage(options: {
el.__unbsubLocalStorage = undefined;
};
}
if (options.state) {
ReactiveElement.createProperty(propertyKey, {
noAccessor: true,
...options.stateOptions,
});
}
const descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey);
let newDescriptor: PropertyDescriptor;

View File

@@ -1,4 +1,10 @@
import type { ReactiveElement, PropertyValues } from "lit";
import {
ReactiveElement,
type PropertyDeclaration,
type PropertyValues,
} from "lit";
import { shallowEqual } from "../util/shallow-equal";
/**
* Transform function type.
*/
@@ -17,6 +23,7 @@ type ReactiveTransformElement = ReactiveElement & {
export function transform<T, V>(config: {
transformer: Transformer<T, V>;
watch?: PropertyKey[];
propertyOptions?: PropertyDeclaration;
}) {
return <ElemClass extends ReactiveElement>(
proto: ElemClass,
@@ -77,6 +84,11 @@ export function transform<T, V>(config: {
curWatch.add(propertyKey);
});
}
ReactiveElement.createProperty(propertyKey, {
noAccessor: true,
hasChanged: (v: any, o: any) => !shallowEqual(v, o),
...config.propertyOptions,
});
const descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey);
let newDescriptor: PropertyDescriptor;

View File

@@ -1,11 +1,5 @@
export const canOverrideAlphanumericInput = (composedPath: EventTarget[]) => {
if (
composedPath.some(
(el) =>
"tagName" in el &&
(el.tagName === "HA-MENU" || el.tagName === "HA-CODE-EDITOR")
)
) {
if (composedPath.some((el) => "tagName" in el && el.tagName === "HA-MENU")) {
return false;
}

View File

@@ -600,32 +600,12 @@ export class HaChartBase extends LitElement {
}
private _getSeries() {
const series = ensureArray(this.data).filter(
if (!Array.isArray(this.data)) {
return this.data;
}
return this.data.filter(
(d) => !this._hiddenDatasets.has(String(d.name ?? d.id))
);
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
| YAXisOption
| undefined;
if (yAxis?.type === "log") {
// set <=0 values to null so they render as gaps on a log graph
return series.map((d) =>
d.type === "line"
? {
...d,
data: d.data?.map((v) =>
Array.isArray(v)
? [
v[0],
typeof v[1] !== "number" || v[1] > 0 ? v[1] : null,
...v.slice(2),
]
: v
),
}
: d
);
}
return series;
}
private _getDefaultHeight() {

View File

@@ -106,10 +106,6 @@ export class HaSankeyChart extends LitElement {
private _createData = memoizeOne((data: SankeyChartData, width = 0) => {
const filteredNodes = data.nodes.filter((n) => n.value > 0);
const indexes = [...new Set(filteredNodes.map((n) => n.index))];
const depthMap = new Map<number, number>();
indexes.sort().forEach((index, i) => {
depthMap.set(index, i);
});
const links = this._processLinks(filteredNodes, data.links);
const sectionWidth = width / indexes.length;
const labelSpace = sectionWidth - NODE_SIZE - LABEL_DISTANCE;
@@ -123,7 +119,7 @@ export class HaSankeyChart extends LitElement {
itemStyle: {
color: node.color,
},
depth: depthMap.get(node.index),
depth: node.index,
})),
links,
draggable: false,

View File

@@ -603,7 +603,7 @@ export class HaDataTable extends LitElement {
.map(
([key2, column2], i) =>
html`${i !== 0
? " · "
? " "
: nothing}${column2.template
? column2.template(row)
: row[key2]}`

View File

@@ -8,7 +8,7 @@ import type { HaEntityComboBoxEntityFilterFunc } from "./ha-entity-combo-box";
import "./ha-entity-picker";
@customElement("ha-entities-picker")
class HaEntitiesPicker extends LitElement {
class HaEntitiesPickerLight extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Array }) public value?: string[];
@@ -17,10 +17,6 @@ class HaEntitiesPicker extends LitElement {
@property({ type: Boolean }) public required = false;
@property() public label?: string;
@property() public placeholder?: string;
@property() public helper?: string;
/**
@@ -71,6 +67,11 @@ class HaEntitiesPicker extends LitElement {
@property({ type: Array, attribute: "exclude-entities" })
public excludeEntities?: string[];
@property({ attribute: "picked-entity-label" })
public pickedEntityLabel?: string;
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
@property({ attribute: false })
public entityFilter?: HaEntityComboBoxEntityFilterFunc;
@@ -83,7 +84,6 @@ class HaEntitiesPicker extends LitElement {
const currentEntities = this._currentEntities;
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
${currentEntities.map(
(entityId) => html`
<div>
@@ -99,6 +99,7 @@ class HaEntitiesPicker extends LitElement {
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this.entityFilter}
.value=${entityId}
.label=${this.pickedEntityLabel}
.disabled=${this.disabled}
.createDomains=${this.createDomains}
@value-changed=${this._entityChanged}
@@ -120,7 +121,7 @@ class HaEntitiesPicker extends LitElement {
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this.entityFilter}
.placeholder=${this.placeholder}
.label=${this.pickEntityLabel}
.helper=${this.helper}
.disabled=${this.disabled}
.createDomains=${this.createDomains}
@@ -197,15 +198,11 @@ class HaEntitiesPicker extends LitElement {
div {
margin-top: 8px;
}
label {
display: block;
margin: 0 0 8px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-entities-picker": HaEntitiesPicker;
"ha-entities-picker": HaEntitiesPickerLight;
}
}

View File

@@ -73,20 +73,16 @@ class HaEntityAttributePicker extends LitElement {
return nothing;
}
const stateObj = this.hass.states[this.entityId!] as HassEntity | undefined;
return html`
<ha-combo-box
.hass=${this.hass}
.value=${this.value
? stateObj
? computeAttributeNameDisplay(
this.hass.localize,
stateObj,
this.hass.entities,
this.value
)
: this.value
? computeAttributeNameDisplay(
this.hass.localize,
this.hass.states[this.entityId!],
this.hass.entities,
this.value
)
: ""}
.autofocus=${this.autofocus}
.label=${this.label ??

View File

@@ -5,6 +5,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeAreaName } from "../../common/entity/compute_area_name";
@@ -29,17 +30,28 @@ import "../ha-icon-button";
import "../ha-svg-icon";
import "./state-badge";
interface EntityComboBoxItem {
const FAKE_ENTITY: HassEntity = {
entity_id: "",
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
attributes: {},
};
interface EntityComboBoxItem extends HassEntity {
// Force empty label to always display empty value by default in the search field
id: string;
label: "";
primary: string;
secondary?: string;
domain_name?: string;
search_labels?: string[];
translated_domain?: string;
show_entity_id?: boolean;
entity_name?: string;
area_name?: string;
device_name?: string;
friendly_name?: string;
sorting_label?: string;
icon_path?: string;
stateObj?: HassEntity;
}
export type HaEntityComboBoxEntityFilterFunc = (entity: HassEntity) => boolean;
@@ -47,6 +59,22 @@ export type HaEntityComboBoxEntityFilterFunc = (entity: HassEntity) => boolean;
const CREATE_ID = "___create-new-entity___";
const NO_ENTITIES_ID = "___no-entities___";
const DOMAIN_STYLE = styleMap({
fontSize: "var(--ha-font-size-s)",
fontWeight: "var(--ha-font-weight-normal)",
lineHeight: "var(--ha-line-height-normal)",
alignSelf: "flex-end",
maxWidth: "30%",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
});
const ENTITY_ID_STYLE = styleMap({
fontFamily: "var(--code-font-family, monospace)",
fontSize: "var(--ha-font-size-xs)",
});
@customElement("ha-entity-combo-box")
export class HaEntityComboBox extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -149,41 +177,33 @@ export class HaEntityComboBox extends LitElement {
private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = (
item,
{ index }
) => {
const showEntityId = this.hass.userData?.showEntityIdPicker;
return html`
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
${item.icon_path
? html`
<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>
`
: html`
<state-badge
slot="start"
.stateObj=${item.stateObj}
.hass=${this.hass}
></state-badge>
`}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.stateObj && showEntityId
? html`
<span slot="supporting-text" class="code">
${item.stateObj.entity_id}
</span>
`
: nothing}
${item.domain_name && !showEntityId
? html`
<div slot="trailing-supporting-text">${item.domain_name}</div>
`
: nothing}
</ha-combo-box-item>
`;
};
) => html`
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
${item.icon_path
? html`<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>`
: html`
<state-badge
slot="start"
.stateObj=${item}
.hass=${this.hass}
></state-badge>
`}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.entity_id && item.show_entity_id
? html`<span slot="supporting-text" style=${ENTITY_ID_STYLE}
>${item.entity_id}</span
>`
: nothing}
${item.translated_domain && !item.show_entity_id
? html`<div slot="trailing-supporting-text" style=${DOMAIN_STYLE}>
${item.translated_domain}
</div>`
: nothing}
</ha-combo-box-item>
`;
private _getItems = memoizeOne(
(
@@ -198,7 +218,7 @@ export class HaEntityComboBox extends LitElement {
excludeEntities: this["excludeEntities"],
createDomains: this["createDomains"]
): EntityComboBoxItem[] => {
let items: EntityComboBoxItem[] = [];
let states: EntityComboBoxItem[] = [];
let entityIds = Object.keys(hass.states);
@@ -216,8 +236,9 @@ export class HaEntityComboBox extends LitElement {
);
return {
id: CREATE_ID + domain,
...FAKE_ENTITY,
label: "",
entity_id: CREATE_ID + domain,
primary: primary,
secondary: this.hass.localize(
"ui.components.entity.entity-picker.new_entity"
@@ -230,8 +251,9 @@ export class HaEntityComboBox extends LitElement {
if (!entityIds.length) {
return [
{
id: NO_ENTITIES_ID,
...FAKE_ENTITY,
label: "",
entity_id: NO_ENTITIES_ID,
primary: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
@@ -267,7 +289,7 @@ export class HaEntityComboBox extends LitElement {
const isRTL = computeRTL(this.hass);
items = entityIds
states = entityIds
.map<EntityComboBoxItem>((entityId) => {
const stateObj = hass!.states[entityId];
@@ -278,32 +300,30 @@ export class HaEntityComboBox extends LitElement {
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const domainName = domainToName(
this.hass.localize,
computeDomain(entityId)
);
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const translatedDomain = domainToName(
this.hass.localize,
computeDomain(entityId)
);
return {
id: entityId,
...hass!.states[entityId],
label: "",
primary: primary,
secondary: secondary,
domain_name: domainName,
sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
search_labels: [
entityName,
deviceName,
areaName,
domainName,
friendlyName,
entityId,
].filter(Boolean) as string[],
stateObj: stateObj,
secondary:
secondary ||
this.hass.localize("ui.components.device-picker.no_area"),
translated_domain: translatedDomain,
sorting_label: [deviceName, entityName].filter(Boolean).join("-"),
entity_name: entityName || deviceName,
area_name: areaName,
device_name: deviceName,
friendly_name: friendlyName,
show_entity_id: hass.userData?.showEntityIdPicker,
};
})
.sort((entityA, entityB) =>
@@ -315,43 +335,41 @@ export class HaEntityComboBox extends LitElement {
);
if (includeDeviceClasses) {
items = items.filter(
(item) =>
states = states.filter(
(stateObj) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj?.attributes.device_class &&
includeDeviceClasses.includes(
item.stateObj.attributes.device_class
))
stateObj.entity_id === this.value ||
(stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class))
);
}
if (includeUnitOfMeasurement) {
items = items.filter(
(item) =>
states = states.filter(
(stateObj) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj?.attributes.unit_of_measurement &&
stateObj.entity_id === this.value ||
(stateObj.attributes.unit_of_measurement &&
includeUnitOfMeasurement.includes(
item.stateObj.attributes.unit_of_measurement
stateObj.attributes.unit_of_measurement
))
);
}
if (entityFilter) {
items = items.filter(
(item) =>
states = states.filter(
(stateObj) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj && entityFilter!(item.stateObj))
stateObj.entity_id === this.value || entityFilter!(stateObj)
);
}
if (!items.length) {
if (!states.length) {
return [
{
id: NO_ENTITIES_ID,
...FAKE_ENTITY,
label: "",
entity_id: NO_ENTITIES_ID,
primary: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
@@ -362,10 +380,10 @@ export class HaEntityComboBox extends LitElement {
}
if (createItems?.length) {
items.push(...createItems);
states.push(...createItems);
}
return items;
return states;
}
);
@@ -408,7 +426,7 @@ export class HaEntityComboBox extends LitElement {
protected render(): TemplateResult {
return html`
<ha-combo-box
item-value-path="id"
item-value-path="entity_id"
.hass=${this.hass}
.value=${this._value}
.label=${this.label === undefined
@@ -460,7 +478,17 @@ export class HaEntityComboBox extends LitElement {
}
private _fuseIndex = memoizeOne((states: EntityComboBoxItem[]) =>
Fuse.createIndex(["search_labels"], states)
Fuse.createIndex(
[
"entity_name",
"device_name",
"area_name",
"translated_domain",
"friendly_name", // for backwards compatibility
"entity_id", // for technical search
],
states
)
);
private _filterChanged(ev: CustomEvent): void {
@@ -477,8 +505,9 @@ export class HaEntityComboBox extends LitElement {
if (results.length === 0) {
target.filteredItems = [
{
id: NO_ENTITIES_ID,
...FAKE_ENTITY,
label: "",
entity_id: NO_ENTITIES_ID,
primary: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),

View File

@@ -162,7 +162,10 @@ export class HaEntityPicker extends LitElement {
slot="start"
></state-badge>
<span slot="headline">${primary}</span>
<span slot="supporting-text">${secondary}</span>
<span slot="supporting-text">
${secondary ||
this.hass.localize("ui.components.device-picker.no_area")}
</span>
${showClearIcon
? html`<ha-icon-button
class="clear"
@@ -177,7 +180,7 @@ export class HaEntityPicker extends LitElement {
protected render() {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
${this.label ? html`<p class="label">${this.label}</p>` : nothing}
<div class="container">
${!this._opened
? html`<ha-combo-box-item
@@ -316,7 +319,7 @@ export class HaEntityPicker extends LitElement {
--mdc-icon-size: 20px;
width: 32px;
}
label {
.label {
display: block;
margin: 0 0 8px;
}

View File

@@ -1,481 +0,0 @@
import { mdiChartLine, mdiHelpCircle, mdiShape } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import Fuse from "fuse.js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { computeStateName } from "../../common/entity/compute_state_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { computeRTL } from "../../common/util/compute_rtl";
import { domainToName } from "../../data/integration";
import type { StatisticsMetaData } from "../../data/recorder";
import { getStatisticIds, getStatisticLabel } from "../../data/recorder";
import { HaFuse } from "../../resources/fuse";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box-item";
import "../ha-svg-icon";
import "./state-badge";
import { documentationUrl } from "../../util/documentation-url";
type StatisticItemType = "entity" | "external" | "no_state";
interface StatisticItem {
// Force empty label to always display empty value by default in the search field
id: string;
statistic_id?: string;
label: "";
primary: string;
secondary?: string;
search_labels?: string[];
sorting_label?: string;
icon_path?: string;
type?: StatisticItemType;
stateObj?: HassEntity;
}
const MISSING_ID = "___missing-entity___";
const TYPE_ORDER = ["entity", "external", "no_state"] as StatisticItemType[];
@customElement("ha-statistic-combo-box")
export class HaStatisticComboBox extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property({ attribute: "statistic-types" })
public statisticTypes?: "mean" | "sum";
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@property({ attribute: false, type: Array })
public statisticIds?: StatisticsMetaData[];
@property({ type: Boolean }) public disabled = false;
/**
* Show only statistics natively stored with these units of measurements.
* @type {Array}
* @attr include-statistics-unit-of-measurement
*/
@property({
type: Array,
attribute: "include-statistics-unit-of-measurement",
})
public includeStatisticsUnitOfMeasurement?: string | string[];
/**
* Show only statistics with these unit classes.
* @attr include-unit-class
*/
@property({ attribute: "include-unit-class" })
public includeUnitClass?: string | string[];
/**
* Show only statistics with these device classes.
* @attr include-device-class
*/
@property({ attribute: "include-device-class" })
public includeDeviceClass?: string | string[];
/**
* Show only statistics on entities.
* @type {Boolean}
* @attr entities-only
*/
@property({ type: Boolean, attribute: "entities-only" })
public entitiesOnly = false;
/**
* List of statistics to be excluded.
* @type {Array}
* @attr exclude-statistics
*/
@property({ type: Array, attribute: "exclude-statistics" })
public excludeStatistics?: string[];
@property({ attribute: false }) public helpMissingEntityUrl =
"/more-info/statistics/";
@state() private _opened = false;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _initialItems = false;
private _items: StatisticItem[] = [];
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.hass.loadBackendTranslation("title");
}
private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = (
item,
{ index }
) => {
const showEntityId = this.hass.userData?.showEntityIdPicker;
return html`
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
${item.icon_path
? html`
<ha-svg-icon
style="margin: 0 4px"
slot="start"
.path=${item.icon_path}
></ha-svg-icon>
`
: item.stateObj
? html`
<state-badge
slot="start"
.stateObj=${item.stateObj}
.hass=${this.hass}
></state-badge>
`
: nothing}
<span slot="headline">${item.primary} </span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.id && showEntityId
? html`<span slot="supporting-text" class="code">
${item.statistic_id}
</span>`
: nothing}
</ha-combo-box-item>
`;
};
private _getItems = memoizeOne(
(
_opened: boolean,
hass: this["hass"],
statisticIds: StatisticsMetaData[],
includeStatisticsUnitOfMeasurement?: string | string[],
includeUnitClass?: string | string[],
includeDeviceClass?: string | string[],
entitiesOnly?: boolean,
excludeStatistics?: string[],
value?: string
): StatisticItem[] => {
if (!statisticIds.length) {
return [
{
id: "",
label: "",
primary: this.hass.localize(
"ui.components.statistic-picker.no_statistics"
),
},
];
}
if (includeStatisticsUnitOfMeasurement) {
const includeUnits: (string | null)[] = ensureArray(
includeStatisticsUnitOfMeasurement
);
statisticIds = statisticIds.filter((meta) =>
includeUnits.includes(meta.statistics_unit_of_measurement)
);
}
if (includeUnitClass) {
const includeUnitClasses: (string | null)[] =
ensureArray(includeUnitClass);
statisticIds = statisticIds.filter((meta) =>
includeUnitClasses.includes(meta.unit_class)
);
}
if (includeDeviceClass) {
const includeDeviceClasses: (string | null)[] =
ensureArray(includeDeviceClass);
statisticIds = statisticIds.filter((meta) => {
const stateObj = this.hass.states[meta.statistic_id];
if (!stateObj) {
return true;
}
return includeDeviceClasses.includes(
stateObj.attributes.device_class || ""
);
});
}
const isRTL = computeRTL(this.hass);
const output: StatisticItem[] = [];
statisticIds.forEach((meta) => {
if (
excludeStatistics &&
meta.statistic_id !== value &&
excludeStatistics.includes(meta.statistic_id)
) {
return;
}
const stateObj = this.hass.states[meta.statistic_id];
if (!stateObj) {
if (!entitiesOnly) {
const id = meta.statistic_id;
const label = getStatisticLabel(this.hass, meta.statistic_id, meta);
const type =
meta.statistic_id.includes(":") &&
!meta.statistic_id.includes(".")
? "external"
: "no_state";
if (type === "no_state") {
output.push({
id,
primary: label,
secondary: this.hass.localize(
"ui.components.statistic-picker.no_state"
),
label: "",
type,
sorting_label: label,
search_labels: [label, id],
icon_path: mdiShape,
});
} else if (type === "external") {
const domain = id.split(":")[0];
const domainName = domainToName(this.hass.localize, domain);
output.push({
id,
statistic_id: id,
primary: label,
secondary: domainName,
label: "",
type,
sorting_label: label,
search_labels: [label, domainName, id],
icon_path: mdiChartLine,
});
}
}
return;
}
const id = meta.statistic_id;
const { area, device } = getEntityContext(stateObj, hass);
const friendlyName = computeStateName(stateObj); // Keep this for search
const entityName = computeEntityName(stateObj, hass);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const primary = entityName || deviceName || id;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
output.push({
id,
statistic_id: id,
label: "",
primary,
secondary,
stateObj: stateObj,
type: "entity",
sorting_label: [deviceName, entityName].join("_"),
search_labels: [
entityName,
deviceName,
areaName,
friendlyName,
id,
].filter(Boolean) as string[],
});
});
if (!output.length) {
return [
{
id: "",
primary: this.hass.localize(
"ui.components.statistic-picker.no_match"
),
label: "",
},
];
}
if (output.length > 1) {
output.sort((a, b) => {
const aPrefix = TYPE_ORDER.indexOf(a.type || "no_state");
const bPrefix = TYPE_ORDER.indexOf(b.type || "no_state");
return caseInsensitiveStringCompare(
`${aPrefix}_${a.sorting_label || ""}`,
`${bPrefix}_${b.sorting_label || ""}`,
this.hass.locale.language
);
});
}
output.push({
id: MISSING_ID,
primary: this.hass.localize(
"ui.components.statistic-picker.missing_entity"
),
label: "",
icon_path: mdiHelpCircle,
});
return output;
}
);
public async open() {
await this.updateComplete;
await this.comboBox?.open();
}
public async focus() {
await this.updateComplete;
await this.comboBox?.focus();
}
protected shouldUpdate(changedProps: PropertyValues) {
if (
changedProps.has("value") ||
changedProps.has("label") ||
changedProps.has("disabled")
) {
return true;
}
return !(!changedProps.has("_opened") && this._opened);
}
public willUpdate(changedProps: PropertyValues) {
if (
(!this.hasUpdated && !this.statisticIds) ||
changedProps.has("statisticTypes")
) {
this._getStatisticIds();
}
if (
this.statisticIds &&
(!this._initialItems || (changedProps.has("_opened") && this._opened))
) {
this._items = this._getItems(
this._opened,
this.hass,
this.statisticIds!,
this.includeStatisticsUnitOfMeasurement,
this.includeUnitClass,
this.includeDeviceClass,
this.entitiesOnly,
this.excludeStatistics,
this.value
);
if (this._initialItems) {
this.comboBox.filteredItems = this._items;
}
this._initialItems = true;
}
}
protected render(): TemplateResult | typeof nothing {
if (this._items.length === 0) {
return nothing;
}
return html`
<ha-combo-box
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.statistic-picker.statistic")
: this.label}
.value=${this._value}
.renderer=${this._rowRenderer}
.disabled=${this.disabled}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._items}
item-value-path="id"
item-id-path="id"
item-label-path="label"
@opened-changed=${this._openedChanged}
@value-changed=${this._statisticChanged}
@filter-changed=${this._filterChanged}
></ha-combo-box>
`;
}
private async _getStatisticIds() {
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
}
private get _value() {
return this.value || "";
}
private _statisticChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
let newValue = ev.detail.value;
if (newValue === MISSING_ID) {
newValue = "";
window.open(
documentationUrl(this.hass, this.helpMissingEntityUrl),
"_blank"
);
}
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _fuseIndex = memoizeOne((states: StatisticItem[]) =>
Fuse.createIndex(["search_labels"], states)
);
private _filterChanged(ev: CustomEvent): void {
if (!this._opened) return;
const target = ev.target as HaComboBox;
const filterString = ev.detail.value.trim().toLowerCase() as string;
const index = this._fuseIndex(this._items);
const fuse = new HaFuse(this._items, {}, index);
const results = fuse.multiTermsSearch(filterString);
if (results) {
target.filteredItems = results.map((result) => result.item);
} else {
target.filteredItems = this._items;
}
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-statistic-combo-box": HaStatisticComboBox;
}
}

View File

@@ -1,66 +1,65 @@
import { mdiChartLine, mdiClose, mdiMenuDown, mdiShape } from "@mdi/js";
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
import { mdiChartLine } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import Fuse from "fuse.js";
import type { HassEntity } from "home-assistant-js-websocket";
import {
css,
html,
LitElement,
nothing,
type CSSResultGroup,
type PropertyValues,
} from "lit";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { computeStateName } from "../../common/entity/compute_state_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { computeRTL } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce";
import { domainToName } from "../../data/integration";
import {
getStatisticIds,
getStatisticLabel,
type StatisticsMetaData,
} from "../../data/recorder";
import type { HomeAssistant } from "../../types";
import type { StatisticsMetaData } from "../../data/recorder";
import { getStatisticIds, getStatisticLabel } from "../../data/recorder";
import { HaFuse } from "../../resources/fuse";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box-item";
import "../ha-icon-button";
import type { HaMdListItem } from "../ha-md-list-item";
import "../ha-svg-icon";
import "./ha-entity-combo-box";
import type { HaEntityComboBox } from "./ha-entity-combo-box";
import "./ha-statistic-combo-box";
import "./state-badge";
type StatisticItemType = "entity" | "external" | "no_state";
interface StatisticItem {
id: string;
label: string;
primary: string;
secondary?: string;
show_entity_id?: boolean;
entity_name?: string;
area_name?: string;
device_name?: string;
friendly_name?: string;
sorting_label?: string;
state?: HassEntity;
type?: StatisticItemType;
iconPath?: string;
stateObj?: HassEntity;
}
const TYPE_ORDER = ["entity", "external", "no_state"] as StatisticItemType[];
const ENTITY_ID_STYLE = styleMap({
fontFamily: "var(--code-font-family, monospace)",
fontSize: "11px",
});
@customElement("ha-statistic-picker")
export class HaStatisticPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property() public label?: string;
@property() public value?: string;
@property() public helper?: string;
@property() public placeholder?: string;
@property({ attribute: "statistic-types" })
public statisticTypes?: "mean" | "sum";
@@ -70,6 +69,8 @@ export class HaStatisticPicker extends LitElement {
@property({ attribute: false, type: Array })
public statisticIds?: StatisticsMetaData[];
@property({ type: Boolean }) public disabled = false;
/**
* Show only statistics natively stored with these units of measurements.
* @type {Array}
@@ -111,15 +112,251 @@ export class HaStatisticPicker extends LitElement {
@property({ type: Array, attribute: "exclude-statistics" })
public excludeStatistics?: string[];
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@query("#anchor") private _anchor?: HaMdListItem;
@query("#input") private _input?: HaEntityComboBox;
@property({ attribute: false }) public helpMissingEntityUrl =
"/more-info/statistics/";
@state() private _opened = false;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _initialItems = false;
private _items: StatisticItem[] = [];
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.hass.loadBackendTranslation("title");
}
private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = (
item,
{ index }
) => html`
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
${!item.state
? html`<ha-svg-icon
style="margin: 0 4px"
slot="start"
.path=${item.iconPath}
></ha-svg-icon>`
: html`
<state-badge
slot="start"
.stateObj=${item.state}
.hass=${this.hass}
></state-badge>
`}
<span slot="headline">${item.primary} </span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.id && item.show_entity_id
? html`
<span slot="supporting-text" style=${ENTITY_ID_STYLE}>
${item.id}
</span>
`
: nothing}
</ha-combo-box-item>
`;
private _getItems = memoizeOne(
(
_opened: boolean,
hass: this["hass"],
statisticIds: StatisticsMetaData[],
includeStatisticsUnitOfMeasurement?: string | string[],
includeUnitClass?: string | string[],
includeDeviceClass?: string | string[],
entitiesOnly?: boolean,
excludeStatistics?: string[],
value?: string
): StatisticItem[] => {
if (!statisticIds.length) {
return [
{
id: "",
label: this.hass.localize(
"ui.components.statistic-picker.no_statistics"
),
primary: this.hass.localize(
"ui.components.statistic-picker.no_statistics"
),
},
];
}
if (includeStatisticsUnitOfMeasurement) {
const includeUnits: (string | null)[] = ensureArray(
includeStatisticsUnitOfMeasurement
);
statisticIds = statisticIds.filter((meta) =>
includeUnits.includes(meta.statistics_unit_of_measurement)
);
}
if (includeUnitClass) {
const includeUnitClasses: (string | null)[] =
ensureArray(includeUnitClass);
statisticIds = statisticIds.filter((meta) =>
includeUnitClasses.includes(meta.unit_class)
);
}
if (includeDeviceClass) {
const includeDeviceClasses: (string | null)[] =
ensureArray(includeDeviceClass);
statisticIds = statisticIds.filter((meta) => {
const stateObj = this.hass.states[meta.statistic_id];
if (!stateObj) {
return true;
}
return includeDeviceClasses.includes(
stateObj.attributes.device_class || ""
);
});
}
const isRTL = computeRTL(this.hass);
const output: StatisticItem[] = [];
statisticIds.forEach((meta) => {
if (
excludeStatistics &&
meta.statistic_id !== value &&
excludeStatistics.includes(meta.statistic_id)
) {
return;
}
const entityState = this.hass.states[meta.statistic_id];
if (!entityState) {
if (!entitiesOnly) {
const id = meta.statistic_id;
const label = getStatisticLabel(this.hass, meta.statistic_id, meta);
const type =
meta.statistic_id.includes(":") &&
!meta.statistic_id.includes(".")
? "external"
: "no_state";
if (type === "no_state") {
output.push({
id,
primary: label,
secondary: this.hass.localize(
"ui.components.statistic-picker.no_state"
),
label,
type,
sorting_label: label,
});
} else if (type === "external") {
const domain = id.split(":")[0];
const domainName = domainToName(this.hass.localize, domain);
output.push({
id,
primary: label,
secondary: domainName,
label,
type,
sorting_label: label,
iconPath: mdiChartLine,
});
}
}
return;
}
const id = meta.statistic_id;
const { area, device } = getEntityContext(entityState, hass);
const friendlyName = computeStateName(entityState); // Keep this for search
const entityName = computeEntityName(entityState, hass);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const primary = entityName || deviceName || id;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
output.push({
id,
primary,
secondary,
label: friendlyName,
state: entityState,
type: "entity",
sorting_label: [deviceName, entityName].join("_"),
entity_name: entityName || deviceName,
area_name: areaName,
device_name: deviceName,
friendly_name: friendlyName,
show_entity_id: hass.userData?.showEntityIdPicker,
});
});
if (!output.length) {
return [
{
id: "",
primary: this.hass.localize(
"ui.components.statistic-picker.no_match"
),
label: this.hass.localize(
"ui.components.statistic-picker.no_match"
),
},
];
}
if (output.length > 1) {
output.sort((a, b) => {
const aPrefix = TYPE_ORDER.indexOf(a.type || "no_state");
const bPrefix = TYPE_ORDER.indexOf(b.type || "no_state");
return caseInsensitiveStringCompare(
`${aPrefix}_${a.sorting_label || ""}`,
`${bPrefix}_${b.sorting_label || ""}`,
this.hass.locale.language
);
});
}
output.push({
id: "__missing",
primary: this.hass.localize(
"ui.components.statistic-picker.missing_entity"
),
label: this.hass.localize(
"ui.components.statistic-picker.missing_entity"
),
});
return output;
}
);
public async open() {
await this.updateComplete;
await this.comboBox?.open();
}
public async focus() {
await this.updateComplete;
await this.comboBox?.focus();
}
protected shouldUpdate(changedProps: PropertyValues) {
if (
changedProps.has("value") ||
changedProps.has("label") ||
changedProps.has("disabled")
) {
return true;
}
return !(!changedProps.has("_opened") && this._opened);
}
public willUpdate(changedProps: PropertyValues) {
if (
(!this.hasUpdated && !this.statisticIds) ||
@@ -127,278 +364,117 @@ export class HaStatisticPicker extends LitElement {
) {
this._getStatisticIds();
}
if (
this.statisticIds &&
(!this._initialItems || (changedProps.has("_opened") && this._opened))
) {
this._items = this._getItems(
this._opened,
this.hass,
this.statisticIds!,
this.includeStatisticsUnitOfMeasurement,
this.includeUnitClass,
this.includeDeviceClass,
this.entitiesOnly,
this.excludeStatistics,
this.value
);
if (this._initialItems) {
this.comboBox.filteredItems = this._items;
}
this._initialItems = true;
}
}
protected render(): TemplateResult | typeof nothing {
if (this._items.length === 0) {
return nothing;
}
return html`
<ha-combo-box
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.statistic-picker.statistic")
: this.label}
.value=${this._value}
.renderer=${this._rowRenderer}
.disabled=${this.disabled}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._items}
item-value-path="id"
item-id-path="id"
item-label-path="label"
@opened-changed=${this._openedChanged}
@value-changed=${this._statisticChanged}
@filter-changed=${this._filterChanged}
></ha-combo-box>
`;
}
private async _getStatisticIds() {
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
}
private _statisticMetaData = memoizeOne(
(statisticId: string, statisticIds: StatisticsMetaData[]) => {
if (!statisticIds) {
return undefined;
}
return statisticIds.find(
(statistic) => statistic.statistic_id === statisticId
);
private get _value() {
return this.value || "";
}
private _statisticChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
let newValue = ev.detail.value;
if (newValue === "__missing") {
newValue = "";
}
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _fuseIndex = memoizeOne((states: StatisticItem[]) =>
Fuse.createIndex(
[
"label",
"entity_name",
"device_name",
"area_name",
"friendly_name", // for backwards compatibility
"id", // for technical search
],
states
)
);
private _renderContent() {
const statisticId = this.value || "";
private _filterChanged(ev: CustomEvent): void {
if (!this._opened) return;
if (!this.value) {
return html`
<span slot="headline" class="placeholder"
>${this.placeholder ??
this.hass.localize(
"ui.components.statistic-picker.placeholder"
)}</span
>
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
`;
}
const target = ev.target as HaComboBox;
const filterString = ev.detail.value.trim().toLowerCase() as string;
const item = this._computeItem(statisticId);
const index = this._fuseIndex(this._items);
const fuse = new HaFuse(this._items, {}, index);
const showClearIcon =
!this.required && !this.disabled && !this.hideClearIcon;
const results = fuse.multiTermsSearch(filterString);
return html`
${item.stateObj
? html`
<state-badge
.hass=${this.hass}
.stateObj=${item.stateObj}
slot="start"
></state-badge>
`
: item.iconPath
? html`<ha-svg-icon
slot="start"
.path=${item.iconPath}
></ha-svg-icon>`
: nothing}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${showClearIcon
? html`<ha-icon-button
class="clear"
slot="end"
@click=${this._clear}
.path=${mdiClose}
></ha-icon-button>`
: nothing}
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
`;
}
private _computeItem(statisticId: string): StatisticItem {
const stateObj = this.hass.states[statisticId];
if (stateObj) {
const { area, device } = getEntityContext(stateObj, this.hass);
const entityName = computeEntityName(stateObj, this.hass);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const isRTL = computeRTL(this.hass);
const primary = entityName || deviceName || statisticId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
return {
primary,
secondary,
stateObj,
};
}
const statistic = this.statisticIds
? this._statisticMetaData(statisticId, this.statisticIds)
: undefined;
if (statistic) {
const type =
statisticId.includes(":") && !statisticId.includes(".")
? "external"
: "no_state";
if (type === "external") {
const label = getStatisticLabel(this.hass, statisticId, statistic);
const domain = statisticId.split(":")[0];
const domainName = domainToName(this.hass.localize, domain);
return {
primary: label,
secondary: domainName,
iconPath: mdiChartLine,
};
}
}
return {
primary: statisticId,
iconPath: mdiShape,
};
}
protected render() {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<div class="container">
${!this._opened
? html`
<ha-combo-box-item
.disabled=${this.disabled}
id="anchor"
type="button"
compact
@click=${this._showPicker}
>
${this._renderContent()}
</ha-combo-box-item>
`
: html`
<ha-statistic-combo-box
id="input"
.hass=${this.hass}
.autofocus=${this.autofocus}
.allowCustomEntity=${this.allowCustomEntity}
.label=${this.hass.localize("ui.common.search")}
.value=${this.value}
.includeStatisticsUnitOfMeasurement=${this
.includeStatisticsUnitOfMeasurement}
.includeUnitClass=${this.includeUnitClass}
.includeDeviceClass=${this.includeDeviceClass}
.statisticTypes=${this.statisticTypes}
.statisticIds=${this.statisticIds}
.excludeStatistics=${this.excludeStatistics}
hide-clear-icon
@opened-changed=${this._debounceOpenedChanged}
@input=${stopPropagation}
></ha-statistic-combo-box>
`}
${this._renderHelper()}
</div>
`;
}
private _renderHelper() {
return this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: nothing;
}
private _clear(e) {
e.stopPropagation();
this.value = undefined;
fireEvent(this, "value-changed", { value: undefined });
fireEvent(this, "change");
}
private async _showPicker() {
if (this.disabled) {
return;
}
this._opened = true;
await this.updateComplete;
this._input?.focus();
this._input?.open();
}
// Multiple calls to _openedChanged can be triggered in quick succession
// when the menu is opened
private _debounceOpenedChanged = debounce(
(ev) => this._openedChanged(ev),
10
);
private async _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
const opened = ev.detail.value;
if (this._opened && !opened) {
this._opened = false;
await this.updateComplete;
this._anchor?.focus();
if (results) {
target.filteredItems = results.map((result) => result.item);
} else {
target.filteredItems = this._items;
}
}
static get styles(): CSSResultGroup {
return [
css`
.container {
position: relative;
display: block;
}
ha-combo-box-item {
background-color: var(--mdc-text-field-fill-color, whitesmoke);
border-radius: 4px;
border-end-end-radius: 0;
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-leading-space: 8px;
--md-list-item-trailing-space: 8px;
--ha-md-list-item-gap: 8px;
/* Remove the default focus ring */
--md-focus-ring-width: 0px;
--md-focus-ring-duration: 0s;
}
/* Add Similar focus style as the text field */
ha-combo-box-item:after {
display: block;
content: "";
position: absolute;
pointer-events: none;
bottom: 0;
left: 0;
right: 0;
height: 1px;
width: 100%;
background-color: var(
--mdc-text-field-idle-line-color,
rgba(0, 0, 0, 0.42)
);
transform:
height 180ms ease-in-out,
background-color 180ms ease-in-out;
}
ha-combo-box-item:focus:after {
height: 2px;
background-color: var(--mdc-theme-primary);
}
ha-combo-box-item ha-svg-icon[slot="start"] {
margin: 0 4px;
}
.clear {
margin: 0 -8px;
--mdc-icon-button-size: 32px;
--mdc-icon-size: 20px;
}
.edit {
--mdc-icon-size: 20px;
width: 32px;
}
label {
display: block;
margin: 0 0 8px;
}
.placeholder {
color: var(--secondary-text-color);
padding: 0 8px;
}
`,
];
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
}
}

View File

@@ -16,11 +16,11 @@ class HaStatisticsPicker extends LitElement {
@property({ attribute: "statistic-types" })
public statisticTypes?: "mean" | "sum";
@property({ type: String })
public label?: string;
@property({ attribute: "picked-statistic-label" })
public pickedStatisticLabel?: string;
@property({ type: String })
public placeholder?: string;
@property({ attribute: "pick-statistic-label" })
public pickStatisticLabel?: string;
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@@ -82,7 +82,6 @@ class HaStatisticsPicker extends LitElement {
: this.statisticTypes;
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
${repeat(
this._currentStatistics,
(statisticId) => statisticId,
@@ -97,6 +96,7 @@ class HaStatisticsPicker extends LitElement {
.value=${statisticId}
.statisticTypes=${includeStatisticTypesCurrent}
.statisticIds=${this.statisticIds}
.label=${this.pickedStatisticLabel}
.excludeStatistics=${this.value}
.allowCustomEntity=${this.allowCustomEntity}
@value-changed=${this._statisticChanged}
@@ -113,7 +113,7 @@ class HaStatisticsPicker extends LitElement {
.includeDeviceClass=${this.includeDeviceClass}
.statisticTypes=${this.statisticTypes}
.statisticIds=${this.statisticIds}
.placeholder=${this.placeholder}
.label=${this.pickStatisticLabel}
.excludeStatistics=${this.value}
.allowCustomEntity=${this.allowCustomEntity}
@value-changed=${this._addStatistic}
@@ -181,10 +181,6 @@ class HaStatisticsPicker extends LitElement {
width: 100%;
margin-top: 8px;
}
label {
display: block;
margin-bottom: 0 0 8px;
}
`;
}

View File

@@ -35,20 +35,6 @@ export class HaComboBoxItem extends HaMdListItem {
width: 32px;
height: 32px;
}
::slotted(.code) {
font-family: var(--ha-font-family-code);
font-size: var(--ha-font-size-xs);
}
[slot="trailing-supporting-text"] {
font-size: var(--ha-font-size-s);
font-weight: var(--ha-font-weight-normal);
line-height: var(--ha-line-height-normal);
align-self: flex-end;
max-width: 30%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
`,
];
}

View File

@@ -30,9 +30,8 @@ class HaLabeledSlider extends LitElement {
@property({ type: Number }) public value?: number;
protected render() {
const title = this._getTitle();
return html`
${title ? html`<div class="title">${title}</div>` : nothing}
<div class="title">${this._getTitle()}</div>
<div class="extra-container"><slot name="extra"></slot></div>
<div class="slider-container">
${this.icon ? html`<ha-icon icon=${this.icon}></ha-icon>` : nothing}
@@ -74,20 +73,17 @@ class HaLabeledSlider extends LitElement {
.slider-container {
display: flex;
align-items: center;
}
ha-icon {
margin-top: 8px;
color: var(--secondary-text-color);
}
ha-slider {
display: flex;
flex-grow: 1;
align-items: center;
background-image: var(--ha-slider-background);
border-radius: 4px;
height: 32px;
}
`;
}

View File

@@ -102,7 +102,7 @@ export class HaLanguagePicker extends LitElement {
localeChanged
) {
this._select.layoutOptions();
if (!this.disabled && this._select.value !== this.value) {
if (this._select.value !== this.value) {
fireEvent(this, "value-changed", { value: this._select.value });
}
if (!this.value) {
@@ -141,10 +141,7 @@ export class HaLanguagePicker extends LitElement {
);
const value =
this.value ??
(this.required && !this.disabled
? languageOptions[0]?.value
: this.value);
this.value ?? (this.required ? languageOptions[0]?.value : this.value);
return html`
<ha-select
@@ -185,7 +182,7 @@ export class HaLanguagePicker extends LitElement {
private _changed(ev): void {
const target = ev.target as HaSelect;
if (this.disabled || target.value === "" || target.value === this.value) {
if (target.value === "" || target.value === this.value) {
return;
}
this.value = target.value;

View File

@@ -6,13 +6,6 @@ import { customElement } from "lit/decorators";
@customElement("ha-outlined-icon-button")
export class HaOutlinedIconButton extends IconButton {
protected override getRenderClasses() {
return {
...super.getRenderClasses(),
outlined: true,
};
}
static override styles = [
css`
.icon-button {

View File

@@ -76,10 +76,10 @@ export class HaEntitySelector extends LitElement {
}
return html`
${this.label ? html`<label>${this.label}</label>` : ""}
<ha-entities-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities}

View File

@@ -210,7 +210,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _unsubPersistentNotifications: UnsubscribeFunc | undefined;
@state()
@storage({
key: "sidebarPanelOrder",
state: true,
@@ -218,7 +217,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
})
private _panelOrder: string[] = [];
@state()
@storage({
key: "sidebarHiddenPanels",
state: true,
@@ -852,8 +850,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
color: var(--sidebar-icon-color);
}
.title {
margin-left: 3px;
margin-inline-start: 3px;
margin-left: 19px;
margin-inline-start: 19px;
margin-inline-end: initial;
width: 100%;
display: none;
@@ -940,6 +938,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
ha-md-list-item .item-text {
display: none;
max-width: calc(100% - 56px);
font-weight: 500;
font-size: 14px;
}

View File

@@ -24,10 +24,6 @@ export class HaToast extends Snackbar {
max-width: 650px;
}
.mdc-snackbar__actions {
color: rgba(255, 255, 255, 0.87);
}
/* Revert the default styles set by mwc-snackbar */
@media (max-width: 480px), (max-width: 344px) {
.mdc-snackbar__surface {

View File

@@ -2,7 +2,6 @@ import "@material/mwc-button/mwc-button";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { mdiContentCopy } from "@mdi/js";
import { storage } from "../../common/decorators/storage";
import { fireEvent } from "../../common/dom/fire_event";
import type {
@@ -18,8 +17,6 @@ import "../ha-language-picker";
import "../ha-tts-voice-picker";
import "../ha-card";
import { fetchCloudStatus } from "../../data/cloud";
import { copyToClipboard } from "../../common/util/copy-clipboard";
import { showToast } from "../../util/toast";
export interface TtsMediaPickedEvent {
item: MediaPlayerItem;
@@ -45,7 +42,6 @@ class BrowseMediaTTS extends LitElement {
@state() private _provider?: TTSEngine;
@state()
@storage({
key: "TtsMessage",
state: true,
@@ -54,69 +50,50 @@ class BrowseMediaTTS extends LitElement {
private _message?: string;
protected render() {
return html`
<ha-card>
<div class="card-content">
<ha-textarea
autogrow
.label=${this.hass.localize(
"ui.components.media-browser.tts.message"
)}
.value=${this._message ||
this.hass.localize(
"ui.components.media-browser.tts.example_message",
{
name: this.hass.user?.name || "Alice",
}
)}
>
</ha-textarea>
${this._provider?.supported_languages?.length
? html` <div class="options">
<ha-language-picker
.hass=${this.hass}
.languages=${this._provider.supported_languages}
.value=${this._language}
required
@value-changed=${this._languageChanged}
></ha-language-picker>
<ha-tts-voice-picker
.hass=${this.hass}
.value=${this._voice}
.engineId=${this._provider.engine_id}
.language=${this._language}
required
@value-changed=${this._voiceChanged}
></ha-tts-voice-picker>
</div>`
: nothing}
</div>
<div class="card-actions">
<mwc-button @click=${this._ttsClicked}>
${this.hass.localize(
`ui.components.media-browser.tts.action_${this.action}`
)}
</mwc-button>
</div>
</ha-card>
${this._voice
? html`
<div class="footer">
${this.hass.localize(
`ui.components.media-browser.tts.selected_voice_id`
)}
<code>${this._voice || "-"}</code>
<ha-icon-button
.path=${mdiContentCopy}
@click=${this._copyVoiceId}
title=${this.hass.localize(
"ui.components.media-browser.tts.copy_voice_id"
)}
></ha-icon-button>
</div>
`
: nothing}
`;
return html`<ha-card>
<div class="card-content">
<ha-textarea
autogrow
.label=${this.hass.localize(
"ui.components.media-browser.tts.message"
)}
.value=${this._message ||
this.hass.localize(
"ui.components.media-browser.tts.example_message",
{
name: this.hass.user?.name || "Alice",
}
)}
>
</ha-textarea>
${this._provider?.supported_languages?.length
? html` <div class="options">
<ha-language-picker
.hass=${this.hass}
.languages=${this._provider.supported_languages}
.value=${this._language}
required
@value-changed=${this._languageChanged}
></ha-language-picker>
<ha-tts-voice-picker
.hass=${this.hass}
.value=${this._voice}
.engineId=${this._provider.engine_id}
.language=${this._language}
required
@value-changed=${this._voiceChanged}
></ha-tts-voice-picker>
</div>`
: nothing}
</div>
<div class="card-actions">
<mwc-button @click=${this._ttsClicked}>
${this.hass.localize(
`ui.components.media-browser.tts.action_${this.action}`
)}
</mwc-button>
</div>
</ha-card> `;
}
protected override willUpdate(changedProps: PropertyValues): void {
@@ -219,14 +196,6 @@ class BrowseMediaTTS extends LitElement {
fireEvent(this, "tts-picked", { item });
}
private async _copyVoiceId(ev) {
ev.preventDefault();
await copyToClipboard(this._voice);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
static override styles = [
buttonLinkStyle,
css`
@@ -248,23 +217,6 @@ class BrowseMediaTTS extends LitElement {
button.link {
color: var(--primary-color);
}
.footer {
font-size: var(--ha-font-size-s);
color: var(--secondary-text-color);
margin: 16px 0;
text-align: center;
}
.footer code {
font-weight: var(--ha-font-weight-bold);
}
.footer {
--mdc-icon-size: 14px;
--mdc-icon-button-size: 24px;
display: flex;
justify-content: center;
align-items: center;
gap: 6px;
}
`,
];
}

View File

@@ -890,18 +890,12 @@ export class HaMediaPlayerBrowse extends LitElement {
display: flex;
flex-direction: row-reverse;
margin-right: 48px;
margin-inline-end: 48px;
margin-inline-start: initial;
direction: var(--direction);
}
.highlight-add-button ha-svg-icon {
position: relative;
top: -0.5em;
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
transform: scaleX(var(--scale-direction));
}
.content {

View File

@@ -85,6 +85,7 @@ class SearchInputOutlined extends LitElement {
display: inline-flex;
/* For iOS */
z-index: 0;
--mdc-icon-button-size: 24px;
}
ha-outlined-text-field {
display: block;
@@ -93,8 +94,6 @@ class SearchInputOutlined extends LitElement {
}
ha-svg-icon,
ha-icon-button {
--mdc-icon-button-size: 24px;
height: var(--mdc-icon-button-size);
display: flex;
color: var(--primary-text-color);
}

View File

@@ -11,8 +11,8 @@ export class HaTimeline extends LitElement {
@property({ type: Boolean, reflect: true }) public raised = false;
@property({ attribute: "not-enabled", reflect: true, type: Boolean })
notEnabled = false;
@property({ attribute: false, reflect: true, type: Boolean }) notEnabled =
false;
@property({ attribute: "last-item", type: Boolean }) public lastItem = false;
@@ -82,7 +82,7 @@ export class HaTimeline extends LitElement {
margin-inline-start: initial;
width: 24px;
}
:host([not-enabled]) ha-svg-icon {
:host([notEnabled]) ha-svg-icon {
opacity: 0.5;
}
ha-svg-icon {

View File

@@ -17,8 +17,8 @@ export class HatGraphNode extends LitElement {
@property({ type: Boolean }) public error = false;
@property({ attribute: "not-enabled", reflect: true, type: Boolean })
notEnabled = false;
@property({ attribute: false, reflect: true, type: Boolean }) notEnabled =
false;
@property({ attribute: "graph-start", reflect: true, type: Boolean })
graphStart = false;
@@ -127,13 +127,13 @@ export class HatGraphNode extends LitElement {
--stroke-clr: var(--hover-clr);
--icon-clr: var(--default-icon-clr);
}
:host([not-enabled]) circle {
:host([notEnabled]) circle {
--stroke-clr: var(--disabled-clr);
}
:host([not-enabled][active]) circle {
:host([notEnabled][active]) circle {
--stroke-clr: var(--disabled-active-clr);
}
:host([not-enabled]:hover) circle {
:host([notEnabled]:hover) circle {
--stroke-clr: var(--disabled-hover-clr);
}
svg:not(.safari) {

View File

@@ -492,25 +492,6 @@ export const getAutomationEditorInitData = () => {
return data;
};
export const isTrigger = (config: unknown): boolean => {
if (!config || typeof config !== "object") {
return false;
}
const trigger = config as Record<string, unknown>;
return (
("trigger" in trigger && typeof trigger.trigger === "string") ||
("platform" in trigger && typeof trigger.platform === "string")
);
};
export const isCondition = (config: unknown): boolean => {
if (!config || typeof config !== "object") {
return false;
}
const condition = config as Record<string, unknown>;
return "condition" in condition && typeof condition.condition === "string";
};
export const subscribeTrigger = (
hass: HomeAssistant,
onChange: (result: {

View File

@@ -1,4 +1,4 @@
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
import type { HassConfig } from "home-assistant-js-websocket";
import { ensureArray } from "../common/array/ensure-array";
import {
formatDurationLong,
@@ -155,7 +155,7 @@ const tryDescribeTrigger = (
const stateObj = Array.isArray(trigger.entity_id)
? hass.states[trigger.entity_id[0]]
: (hass.states[trigger.entity_id] as HassEntity | undefined);
: hass.states[trigger.entity_id];
if (Array.isArray(trigger.entity_id)) {
for (const entity of trigger.entity_id.values()) {
@@ -172,14 +172,12 @@ const tryDescribeTrigger = (
}
const attribute = trigger.attribute
? stateObj
? computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
trigger.attribute
)
: trigger.attribute
? computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
trigger.attribute
)
: undefined;
const duration = trigger.for
@@ -234,15 +232,13 @@ const tryDescribeTrigger = (
if (trigger.attribute) {
const stateObj = Array.isArray(trigger.entity_id)
? hass.states[trigger.entity_id[0]]
: (hass.states[trigger.entity_id] as HassEntity | undefined);
attribute = stateObj
? computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
trigger.attribute
)
: trigger.attribute;
: hass.states[trigger.entity_id];
attribute = computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
trigger.attribute
);
}
const entityArray: string[] = ensureArray(trigger.entity_id);
@@ -254,7 +250,7 @@ const tryDescribeTrigger = (
}
}
const stateObj = hass.states[entityArray[0]] as HassEntity | undefined;
const stateObj = hass.states[entityArray[0]];
let fromChoice = "other";
let fromString = "";
@@ -270,17 +266,15 @@ const tryDescribeTrigger = (
const from: string[] = [];
for (const state of fromArray) {
from.push(
stateObj
? trigger.attribute
? hass
.formatEntityAttributeValue(
stateObj,
trigger.attribute,
state
)
.toString()
: hass.formatEntityState(stateObj, state)
: state
trigger.attribute
? hass
.formatEntityAttributeValue(
stateObj,
trigger.attribute,
state
)
.toString()
: hass.formatEntityState(stateObj, state)
);
}
if (from.length !== 0) {
@@ -304,17 +298,15 @@ const tryDescribeTrigger = (
const to: string[] = [];
for (const state of toArray) {
to.push(
stateObj
? trigger.attribute
? hass
.formatEntityAttributeValue(
stateObj,
trigger.attribute,
state
)
.toString()
: hass.formatEntityState(stateObj, state).toString()
: state
trigger.attribute
? hass
.formatEntityAttributeValue(
stateObj,
trigger.attribute,
state
)
.toString()
: hass.formatEntityState(stateObj, state).toString()
);
}
if (to.length !== 0) {
@@ -733,9 +725,7 @@ const tryDescribeTrigger = (
if (localized) {
return localized;
}
const stateObj = hass.states[config.entity_id as string] as
| HassEntity
| undefined;
const stateObj = hass.states[config.entity_id as string];
return `${stateObj ? computeStateName(stateObj) : config.entity_id} ${
config.type
}`;
@@ -904,15 +894,13 @@ const tryDescribeCondition = (
if (condition.attribute) {
const stateObj = Array.isArray(condition.entity_id)
? hass.states[condition.entity_id[0]]
: (hass.states[condition.entity_id] as HassEntity | undefined);
attribute = stateObj
? computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
condition.attribute
)
: condition.attribute;
: hass.states[condition.entity_id];
attribute = computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
condition.attribute
);
}
const entities: string[] = [];
@@ -931,40 +919,37 @@ const tryDescribeCondition = (
}
const states: string[] = [];
const stateObj = hass.states[
Array.isArray(condition.entity_id)
? condition.entity_id[0]
: condition.entity_id
] as HassEntity | undefined;
const stateObj =
hass.states[
Array.isArray(condition.entity_id)
? condition.entity_id[0]
: condition.entity_id
];
if (Array.isArray(condition.state)) {
for (const state of condition.state.values()) {
states.push(
stateObj
? condition.attribute
? hass
.formatEntityAttributeValue(
stateObj,
condition.attribute,
state
)
.toString()
: hass.formatEntityState(stateObj, state)
: state
);
}
} else if (condition.state !== "") {
states.push(
stateObj
? condition.attribute
condition.attribute
? hass
.formatEntityAttributeValue(
stateObj,
condition.attribute,
condition.state
state
)
.toString()
: hass.formatEntityState(stateObj, condition.state.toString())
: condition.state.toString()
: hass.formatEntityState(stateObj, state)
);
}
} else if (condition.state !== "") {
states.push(
condition.attribute
? hass
.formatEntityAttributeValue(
stateObj,
condition.attribute,
condition.state
)
.toString()
: hass.formatEntityState(stateObj, condition.state.toString())
);
}
@@ -994,7 +979,7 @@ const tryDescribeCondition = (
// Numeric State Condition
if (condition.condition === "numeric_state" && condition.entity_id) {
const entity_ids = ensureArray(condition.entity_id);
const stateObj = hass.states[entity_ids[0]] as HassEntity | undefined;
const stateObj = hass.states[entity_ids[0]];
const entity = formatListWithAnds(
hass.locale,
entity_ids.map((id) =>
@@ -1003,14 +988,12 @@ const tryDescribeCondition = (
);
const attribute = condition.attribute
? stateObj
? computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
condition.attribute
)
: condition.attribute
? computeAttributeNameDisplay(
hass.localize,
stateObj,
hass.entities,
condition.attribute
)
: undefined;
if (condition.above !== undefined && condition.below !== undefined) {
@@ -1204,9 +1187,7 @@ const tryDescribeCondition = (
if (localized) {
return localized;
}
const stateObj = hass.states[config.entity_id as string] as
| HassEntity
| undefined;
const stateObj = hass.states[config.entity_id as string];
return `${stateObj ? computeStateName(stateObj) : config.entity_id} ${
config.type
}`;

View File

@@ -959,13 +959,21 @@ const computeConsumptionDataPartial = (
};
data.timestamps.forEach((t) => {
const used_total =
(data.from_grid?.[t] || 0) +
(data.solar?.[t] || 0) +
(data.from_battery?.[t] || 0) -
(data.to_grid?.[t] || 0) -
(data.to_battery?.[t] || 0);
outData.used_total[t] = used_total;
outData.total.used_total += used_total;
const {
grid_to_battery,
battery_to_grid,
used_solar,
used_grid,
used_battery,
used_total,
solar_to_battery,
solar_to_grid,
} = computeConsumptionSingle({
@@ -976,8 +984,6 @@ const computeConsumptionDataPartial = (
from_battery: data.from_battery && (data.from_battery[t] ?? 0),
});
outData.used_total[t] = used_total;
outData.total.used_total += used_total;
outData.grid_to_battery[t] = grid_to_battery;
outData.total.grid_to_battery += grid_to_battery;
outData.battery_to_grid![t] = battery_to_grid;
@@ -1011,20 +1017,12 @@ export const computeConsumptionSingle = (data: {
used_solar: number;
used_grid: number;
used_battery: number;
used_total: number;
} => {
let to_grid = Math.max(data.to_grid || 0, 0);
let to_battery = Math.max(data.to_battery || 0, 0);
let solar = Math.max(data.solar || 0, 0);
let from_grid = Math.max(data.from_grid || 0, 0);
let from_battery = Math.max(data.from_battery || 0, 0);
const used_total =
(from_grid || 0) +
(solar || 0) +
(from_battery || 0) -
(to_grid || 0) -
(to_battery || 0);
const to_grid = data.to_grid;
const to_battery = data.to_battery;
const solar = data.solar;
const from_grid = data.from_grid;
const from_battery = data.from_battery;
let used_solar = 0;
let grid_to_battery = 0;
@@ -1033,57 +1031,47 @@ export const computeConsumptionSingle = (data: {
let solar_to_grid = 0;
let used_battery = 0;
let used_grid = 0;
if (solar == null) {
if (to_battery != null) {
grid_to_battery = to_battery;
}
if (to_grid != null) {
battery_to_grid = to_grid;
}
} else if (to_grid != null || to_battery != null) {
used_solar = (solar || 0) - (to_grid || 0) - (to_battery || 0);
if (used_solar < 0) {
if (to_battery != null) {
grid_to_battery = Math.min(used_solar * -1, from_grid || 0, to_battery);
}
if (to_grid != null) {
battery_to_grid = Math.min(to_grid - solar, from_battery || 0, to_grid);
}
used_solar = 0;
}
}
let used_total_remaining = Math.max(used_total, 0);
// Consumption Priority
// Battery_Out -> Grid_Out
// Solar -> Grid_Out
// Solar -> Battery_In
// Grid_In -> Battery_In
// Solar -> Consumption
// Battery_Out -> Consumption
// Grid_In -> Consumption
if (from_battery != null) {
used_battery = (from_battery || 0) - battery_to_grid;
}
// Battery_Out -> Grid_Out
battery_to_grid = Math.min(from_battery, to_grid);
from_battery -= battery_to_grid;
to_grid -= battery_to_grid;
if (from_grid != null) {
used_grid = from_grid - grid_to_battery;
}
// Solar -> Grid_Out
solar_to_grid = Math.min(solar, to_grid);
to_grid -= solar_to_grid;
solar -= solar_to_grid;
// Solar -> Battery_In
solar_to_battery = Math.min(solar, to_battery);
to_battery -= solar_to_battery;
solar -= solar_to_battery;
// Grid_In -> Battery_In
grid_to_battery = Math.min(from_grid, to_battery);
from_grid -= grid_to_battery;
to_battery -= grid_to_battery;
// Solar -> Consumption
used_solar = Math.min(used_total_remaining, solar);
used_total_remaining -= used_solar;
solar -= used_solar;
// Battery_Out -> Consumption
used_battery = Math.min(from_battery, used_total_remaining);
from_battery -= used_battery;
used_total_remaining -= used_battery;
// Grid_In -> Consumption
used_grid = Math.min(used_total_remaining, from_grid);
from_grid -= used_grid;
used_total_remaining -= from_grid;
if (solar != null) {
if (to_battery != null) {
solar_to_battery = Math.max(0, (to_battery || 0) - grid_to_battery);
}
if (to_grid != null) {
solar_to_grid = Math.max(0, (to_grid || 0) - battery_to_grid);
}
}
return {
used_solar,
used_grid,
used_battery,
used_total,
grid_to_battery,
battery_to_grid,
solar_to_battery,

View File

@@ -107,70 +107,3 @@ export const DOMAIN_ATTRIBUTES_FORMATERS: Record<
},
},
};
export const NON_NUMERIC_ATTRIBUTES = [
"access_token",
"auto_update",
"available_modes",
"away_mode",
"changed_by",
"code_format",
"color_modes",
"current_activity",
"device_class",
"editable",
"effect_list",
"effect",
"entity_picture",
"event_type",
"event_types",
"fan_mode",
"fan_modes",
"fan_speed_list",
"forecast",
"friendly_name",
"frontend_stream_type",
"has_date",
"has_time",
"hs_color",
"hvac_mode",
"hvac_modes",
"icon",
"media_album_name",
"media_artist",
"media_content_type",
"media_position_updated_at",
"media_title",
"next_dawn",
"next_dusk",
"next_midnight",
"next_noon",
"next_rising",
"next_setting",
"operation_list",
"operation_mode",
"options",
"preset_mode",
"preset_modes",
"release_notes",
"release_summary",
"release_url",
"restored",
"rgb_color",
"rgbw_color",
"shuffle",
"sound_mode_list",
"sound_mode",
"source_list",
"source_type",
"source",
"state_class",
"supported_features",
"swing_mode",
"swing_mode",
"swing_modes",
"title",
"token",
"unit_of_measurement",
"xy_color",
];

View File

@@ -7,7 +7,6 @@ import type { Store } from "home-assistant-js-websocket/dist/store";
import type { DataTableRowData } from "../components/data-table/ha-data-table";
export interface SSDPDiscoveryData extends DataTableRowData {
name: string | undefined;
ssdp_usn: string;
ssdp_st: string;
upnp: Record<string, unknown>;

View File

@@ -1,3 +1,4 @@
import "@material/mwc-button";
import { mdiClose, mdiHelpCircle } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
@@ -176,17 +177,6 @@ class DataEntryFlowDialog extends LitElement {
return nothing;
}
const showDocumentationLink =
([
"form",
"menu",
"external",
"progress",
"data_entry_flow_progressed",
].includes(this._step?.type as any) &&
this._params.manifest?.is_built_in) ||
!!this._params.manifest?.documentation;
return html`
<ha-dialog
open
@@ -201,7 +191,7 @@ class DataEntryFlowDialog extends LitElement {
<step-flow-loading
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
.loadingReason=${this._loading!}
.loadingReason=${this._loading}
.handler=${this._handler}
.step=${this._step}
></step-flow-loading>
@@ -209,18 +199,26 @@ class DataEntryFlowDialog extends LitElement {
: this._step === undefined
? // When we are going to next step, we render 1 round of empty
// to reset the element.
nothing
""
: html`
<div class="dialog-actions">
${showDocumentationLink
${([
"form",
"menu",
"external",
"progress",
"data_entry_flow_progressed",
].includes(this._step?.type as any) &&
this._params.manifest?.is_built_in) ||
this._params.manifest?.documentation
? html`
<a
href=${this._params.manifest!.is_built_in
href=${this._params.manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._params.manifest!.domain}`
`/integrations/${this._params.manifest.domain}`
)
: this._params.manifest!.documentation}
: this._params?.manifest?.documentation}
target="_blank"
rel="noreferrer noopener"
>
@@ -231,7 +229,7 @@ class DataEntryFlowDialog extends LitElement {
</ha-icon-button
></a>
`
: nothing}
: ""}
<ha-icon-button
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@@ -244,7 +242,6 @@ class DataEntryFlowDialog extends LitElement {
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.increasePaddingEnd=${showDocumentationLink}
></step-flow-form>
`
: this._step.type === "external"
@@ -253,7 +250,6 @@ class DataEntryFlowDialog extends LitElement {
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.increasePaddingEnd=${showDocumentationLink}
></step-flow-external>
`
: this._step.type === "abort"
@@ -265,7 +261,6 @@ class DataEntryFlowDialog extends LitElement {
.handler=${this._step.handler}
.domain=${this._params.domain ??
this._step.handler}
.increasePaddingEnd=${showDocumentationLink}
></step-flow-abort>
`
: this._step.type === "progress"
@@ -275,7 +270,6 @@ class DataEntryFlowDialog extends LitElement {
.step=${this._step}
.hass=${this.hass}
.progress=${this._progress}
.increasePaddingEnd=${showDocumentationLink}
></step-flow-progress>
`
: this._step.type === "menu"
@@ -284,7 +278,6 @@ class DataEntryFlowDialog extends LitElement {
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.increasePaddingEnd=${showDocumentationLink}
></step-flow-menu>
`
: html`
@@ -293,8 +286,7 @@ class DataEntryFlowDialog extends LitElement {
.step=${this._step}
.hass=${this.hass}
.navigateToResult=${this._params
.navigateToResult ?? false}
.increasePaddingEnd=${showDocumentationLink}
.navigateToResult}
></step-flow-create-entry>
`}
`}

View File

@@ -22,9 +22,6 @@ class StepFlowAbort extends LitElement {
@property({ attribute: false }) public handler!: string;
@property({ type: Boolean, attribute: "increase-padding-end" })
public increasePaddingEnd = false;
protected firstUpdated(changed: PropertyValues) {
super.firstUpdated(changed);
if (this.step.reason === "missing_credentials") {
@@ -37,7 +34,7 @@ class StepFlowAbort extends LitElement {
return nothing;
}
return html`
<h2 class=${this.increasePaddingEnd ? "end-space" : ""}>
<h2>
${this.params.flowConfig.renderAbortHeader
? this.params.flowConfig.renderAbortHeader(this.hass, this.step)
: this.hass.localize(`component.${this.domain}.title`)}

View File

@@ -36,9 +36,6 @@ class StepFlowCreateEntry extends LitElement {
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
@property({ type: Boolean, attribute: "increase-padding-end" })
public increasePaddingEnd = false;
public navigateToResult = false;
@state() private _deviceUpdate: Record<
@@ -116,7 +113,7 @@ class StepFlowCreateEntry extends LitElement {
this.step.result?.entry_id
);
return html`
<h2 class=${this.increasePaddingEnd ? "end-space" : ""}>
<h2>
${devices.length
? localize("ui.panel.config.integrations.config_flow.assign_area", {
number: devices.length,
@@ -132,73 +129,70 @@ class StepFlowCreateEntry extends LitElement {
)}</span
>`
: nothing}
${devices.length === 0 &&
["options_flow", "repair_flow"].includes(this.flowConfig.flowType)
? nothing
: devices.length === 0
? html`<p>
${localize(
"ui.panel.config.integrations.config_flow.created_config",
{ name: this.step.title }
)}
</p>`
: html`
<div class="devices">
${devices.map(
(device) => html`
<div class="device">
<div class="device-info">
${this.step.result?.domain
? html`<img
slot="graphic"
alt=${domainToName(
this.hass.localize,
this.step.result.domain
)}
src=${brandsUrl({
domain: this.step.result.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>`
${devices.length === 0
? html`<p>
${localize(
"ui.panel.config.integrations.config_flow.created_config",
{ name: this.step.title }
)}
</p>`
: html`
<div class="devices">
${devices.map(
(device) => html`
<div class="device">
<div class="device-info">
${this.step.result?.domain
? html`<img
slot="graphic"
alt=${domainToName(
this.hass.localize,
this.step.result.domain
)}
src=${brandsUrl({
domain: this.step.result.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>`
: nothing}
<div class="device-info-details">
<span>${device.model || device.manufacturer}</span>
${device.model
? html`<span class="secondary">
${device.manufacturer}
</span>`
: nothing}
<div class="device-info-details">
<span>${device.model || device.manufacturer}</span>
${device.model
? html`<span class="secondary">
${device.manufacturer}
</span>`
: nothing}
</div>
</div>
<ha-textfield
.label=${localize(
"ui.panel.config.integrations.config_flow.device_name"
)}
.placeholder=${computeDeviceNameDisplay(
device,
this.hass
)}
.value=${this._deviceUpdate[device.id]?.name ??
computeDeviceName(device)}
@change=${this._deviceNameChanged}
.device=${device.id}
></ha-textfield>
<ha-area-picker
.hass=${this.hass}
.device=${device.id}
.value=${this._deviceUpdate[device.id]?.area ??
device.area_id ??
undefined}
@value-changed=${this._areaPicked}
></ha-area-picker>
</div>
`
)}
</div>
`}
<ha-textfield
.label=${localize(
"ui.panel.config.integrations.config_flow.device_name"
)}
.placeholder=${computeDeviceNameDisplay(
device,
this.hass
)}
.value=${this._deviceUpdate[device.id]?.name ??
computeDeviceName(device)}
@change=${this._deviceNameChanged}
.device=${device.id}
></ha-textfield>
<ha-area-picker
.hass=${this.hass}
.device=${device.id}
.value=${this._deviceUpdate[device.id]?.area ??
device.area_id ??
undefined}
@value-changed=${this._areaPicked}
></ha-area-picker>
</div>
`
)}
</div>
`}
</div>
<div class="buttons">
<mwc-button @click=${this._flowDone}

View File

@@ -15,16 +15,11 @@ class StepFlowExternal extends LitElement {
@property({ attribute: false }) public step!: DataEntryFlowStepExternal;
@property({ type: Boolean, attribute: "increase-padding-end" })
public increasePaddingEnd = false;
protected render(): TemplateResult {
const localize = this.hass.localize;
return html`
<h2 class=${this.increasePaddingEnd ? "end-space" : ""}>
${this.flowConfig.renderExternalStepHeader(this.hass, this.step)}
</h2>
<h2>${this.flowConfig.renderExternalStepHeader(this.hass, this.step)}</h2>
<div class="content">
${this.flowConfig.renderExternalStepDescription(this.hass, this.step)}
<div class="open-button">
@@ -56,9 +51,6 @@ class StepFlowExternal extends LitElement {
.open-button a {
text-decoration: none;
}
h2.end-space {
padding-inline-end: 72px;
}
`,
];
}

View File

@@ -27,9 +27,6 @@ class StepFlowForm extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, attribute: "increase-padding-end" })
public increasePaddingEnd = false;
@state() private _loading = false;
@state() private _stepData?: Record<string, any>;
@@ -46,9 +43,7 @@ class StepFlowForm extends LitElement {
const stepData = this._stepDataProcessed;
return html`
<h2 class=${this.increasePaddingEnd ? "end-space" : ""}>
${this.flowConfig.renderShowFormStepHeader(this.hass, this.step)}
</h2>
<h2>${this.flowConfig.renderShowFormStepHeader(this.hass, this.step)}</h2>
<div class="content" @click=${this._clickHandler}>
${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)}
${this._errorMsg
@@ -283,6 +278,8 @@ class StepFlowForm extends LitElement {
}
h2 {
word-break: break-word;
padding-inline-end: 72px;
direction: var(--direction);
}
`,
];

View File

@@ -17,9 +17,6 @@ class StepFlowMenu extends LitElement {
@property({ attribute: false }) public step!: DataEntryFlowStepMenu;
@property({ type: Boolean, attribute: "increase-padding-end" })
public increasePaddingEnd = false;
protected render(): TemplateResult {
let options: string[];
let translations: Record<string, string>;
@@ -45,9 +42,7 @@ class StepFlowMenu extends LitElement {
);
return html`
<h2 class=${this.increasePaddingEnd ? "end-space" : ""}>
${this.flowConfig.renderMenuHeader(this.hass, this.step)}
</h2>
<h2>${this.flowConfig.renderMenuHeader(this.hass, this.step)}</h2>
${description ? html`<div class="content">${description}</div>` : ""}
<div class="options">
${options.map(

View File

@@ -24,12 +24,9 @@ class StepFlowProgress extends LitElement {
@property({ type: Number })
public progress?: number;
@property({ type: Boolean, attribute: "increase-padding-end" })
public increasePaddingEnd = false;
protected render(): TemplateResult {
return html`
<h2 class=${this.increasePaddingEnd ? "end-space" : ""}>
<h2>
${this.flowConfig.renderShowFormProgressHeader(this.hass, this.step)}
</h2>
<div class="content">

View File

@@ -22,9 +22,6 @@ export const configFlowContentStyles = css`
text-transform: var(--mdc-typography-headline6-text-transform, inherit);
box-sizing: border-box;
}
h2.end-space {
padding-inline-end: 72px;
}
.content,
.preview {

View File

@@ -57,7 +57,7 @@ class MoreInfoCover extends LitElement {
);
if (positionStateDisplay) {
return `${stateDisplay} · ${positionStateDisplay}`;
return `${stateDisplay} ${positionStateDisplay}`;
}
return stateDisplay;
}

View File

@@ -57,7 +57,7 @@ class MoreInfoValve extends LitElement {
);
if (positionStateDisplay) {
return `${stateDisplay} · ${positionStateDisplay}`;
return `${stateDisplay} ${positionStateDisplay}`;
}
return stateDisplay;
}

View File

@@ -15,26 +15,24 @@ import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import Fuse from "fuse.js";
import { canShowPage } from "../../common/config/can_show_page";
import { componentsWithService } from "../../common/config/components_with_service";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import {
computeDeviceName,
computeDeviceNameDisplay,
} from "../../common/entity/compute_device_name";
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
import { computeStateName } from "../../common/entity/compute_state_name";
import { navigate } from "../../common/navigate";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
import { debounce } from "../../common/util/debounce";
import "../../components/ha-icon-button";
import "../../components/ha-label";
import "../../components/ha-list";
import "../../components/ha-list-item";
import "../../components/ha-spinner";
import "../../components/ha-textfield";
import "../../components/ha-tip";
import "../../components/ha-md-list-item";
import { fetchHassioAddonsInfo } from "../../data/hassio/addon";
import { domainToName } from "../../data/integration";
import { getPanelNameTranslationKey } from "../../data/panel";
@@ -46,13 +44,6 @@ import type { HomeAssistant } from "../../types";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeRTL } from "../../common/util/compute_rtl";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { HaFuse } from "../../resources/fuse";
interface QuickBarItem extends ScorableTextItem {
primaryText: string;
@@ -68,9 +59,6 @@ interface CommandItem extends QuickBarItem {
interface EntityItem extends QuickBarItem {
altText: string;
icon?: TemplateResult;
translatedDomain: string;
entityId: string;
friendlyName: string;
}
interface DeviceItem extends QuickBarItem {
@@ -94,7 +82,6 @@ type BaseNavigationCommand = Pick<
QuickBarNavigationItem,
"primaryText" | "path"
>;
@customElement("ha-quick-bar")
export class QuickBar extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -152,11 +139,6 @@ export class QuickBar extends LitElement {
}
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.hass.loadBackendTranslation("title");
}
private _getItems = memoizeOne(
(
mode: QuickBarMode,
@@ -341,67 +323,61 @@ export class QuickBar extends LitElement {
private _renderDeviceItem(item: DeviceItem, index?: number) {
return html`
<ha-md-list-item
class="two-line"
<ha-list-item
.twoline=${Boolean(item.area)}
.item=${item}
index=${ifDefined(index)}
tabindex="0"
type="button"
>
<span slot="headline">${item.primaryText}</span>
<span>${item.primaryText}</span>
${item.area
? html` <span slot="supporting-text">${item.area}</span> `
? html`
<span slot="secondary" class="item-text secondary"
>${item.area}</span
>
`
: nothing}
</ha-md-list-item>
</ha-list-item>
`;
}
private _renderEntityItem(item: EntityItem, index?: number) {
const showEntityId = this.hass.userData?.showEntityIdPicker;
return html`
<ha-md-list-item
class=${showEntityId ? "three-line" : "two-line"}
<ha-list-item
.twoline=${Boolean(item.altText)}
.item=${item}
index=${ifDefined(index)}
graphic="icon"
tabindex="0"
type="button"
>
${item.iconPath
? html`
<ha-svg-icon
.path=${item.iconPath}
class="entity"
slot="start"
slot="graphic"
></ha-svg-icon>
`
: html`<span slot="start">${item.icon}</span>`}
<span slot="headline">${item.primaryText}</span>
: html`<span slot="graphic">${item.icon}</span>`}
<span>${item.primaryText}</span>
${item.altText
? html` <span slot="supporting-text">${item.altText}</span> `
: nothing}
${item.entityId && showEntityId
? html`
<span slot="supporting-text" class="code">${item.entityId}</span>
<span slot="secondary" class="item-text secondary"
>${item.altText}</span
>
`
: nothing}
${item.translatedDomain && !showEntityId
? html`<div slot="trailing-supporting-text">
${item.translatedDomain}
</div>`
: nothing}
</ha-md-list-item>
</ha-list-item>
`;
}
private _renderCommandItem(item: CommandItem, index?: number) {
return html`
<ha-md-list-item
<ha-list-item
.item=${item}
index=${ifDefined(index)}
hasMeta
tabindex="0"
type="button"
>
<span>
<ha-label
@@ -410,10 +386,7 @@ export class QuickBar extends LitElement {
>
${item.iconPath
? html`
<ha-svg-icon
.path=${item.iconPath}
slot="start"
></ha-svg-icon>
<ha-svg-icon .path=${item.iconPath} slot="icon"></ha-svg-icon>
`
: nothing}
${item.categoryText}
@@ -421,7 +394,7 @@ export class QuickBar extends LitElement {
</span>
<span class="command-text">${item.primaryText}</span>
</ha-md-list-item>
</ha-list-item>
`;
}
@@ -448,7 +421,7 @@ export class QuickBar extends LitElement {
}
private _getItemAtIndex(index: number): ListItem | null {
return this.renderRoot.querySelector(`ha-md-list-item[index="${index}"]`);
return this.renderRoot.querySelector(`ha-list-item[index="${index}"]`);
}
private _addSpinnerToCommandItem(index: number): void {
@@ -546,7 +519,7 @@ export class QuickBar extends LitElement {
}
private _handleItemClick(ev) {
const listItem = ev.target.closest("ha-md-list-item");
const listItem = ev.target.closest("ha-list-item");
this._processItemAndCloseDialog(
listItem.item,
Number(listItem.getAttribute("index"))
@@ -582,41 +555,18 @@ export class QuickBar extends LitElement {
}
private _generateEntityItems(): EntityItem[] {
const isRTL = computeRTL(this.hass);
return Object.keys(this.hass.states)
.map((entityId) => {
const stateObj = this.hass.states[entityId];
const { area, device } = getEntityContext(stateObj, this.hass);
const friendlyName = computeStateName(stateObj); // Keep this for search
const entityName = computeEntityName(stateObj, this.hass);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const translatedDomain = domainToName(
this.hass.localize,
computeDomain(entityId)
);
const entityState = this.hass.states[entityId];
const entityItem = {
primaryText: primary,
altText: secondary,
primaryText: computeStateName(entityState),
altText: entityId,
icon: html`
<ha-state-icon
.hass=${this.hass}
.stateObj=${stateObj}
.stateObj=${entityState}
></ha-state-icon>
`,
translatedDomain: translatedDomain,
entityId: entityId,
friendlyName: friendlyName,
action: () => fireEvent(this, "hass-more-info", { entityId }),
};
@@ -896,30 +846,9 @@ export class QuickBar extends LitElement {
});
}
private _fuseIndex = memoizeOne((items: QuickBarItem[]) =>
Fuse.createIndex(
[
"primaryText",
"altText",
"friendlyName",
"translatedDomain",
"entityId", // for technical search
],
items
)
);
private _filterItems = memoizeOne(
(items: QuickBarItem[], filter: string): QuickBarItem[] => {
const index = this._fuseIndex(items);
const fuse = new HaFuse(items, {}, index);
const results = fuse.multiTermsSearch(filter.trim());
if (!results || !results.length) {
return items;
}
return results.map((result) => result.item);
}
(items: QuickBarItem[], filter: string): QuickBarItem[] =>
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
);
static get styles() {
@@ -1001,41 +930,9 @@ export class QuickBar extends LitElement {
direction: var(--direction);
}
ha-md-list-item {
ha-list-item {
width: 100%;
}
/* Fixed height for items because we are use virtualizer */
ha-md-list-item.two-line {
--md-list-item-one-line-container-height: 64px;
--md-list-item-two-line-container-height: 64px;
--md-list-item-top-space: 8px;
--md-list-item-bottom-space: 8px;
}
ha-md-list-item.three-line {
width: 100%;
--md-list-item-one-line-container-height: 72px;
--md-list-item-two-line-container-height: 72px;
--md-list-item-three-line-container-height: 72px;
--md-list-item-top-space: 8px;
--md-list-item-bottom-space: 8px;
}
ha-md-list-item .code {
font-family: var(--ha-font-family-code);
font-size: var(--ha-font-size-xs);
}
ha-md-list-item [slot="trailing-supporting-text"] {
font-size: var(--ha-font-size-s);
font-weight: var(--ha-font-weight-normal);
line-height: var(--ha-line-height-normal);
align-self: flex-end;
max-width: 30%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
--mdc-list-item-graphic-margin: 20px;
}
ha-tip {

View File

@@ -69,17 +69,12 @@ const _SHORTCUTS: Section[] = [
],
},
{
key: "ui.dialogs.shortcuts.automation_script.title",
key: "ui.dialogs.shortcuts.automations.title",
items: [
{
type: "shortcut",
shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" }, "V"],
key: "ui.dialogs.shortcuts.automation_script.paste",
},
{
type: "shortcut",
shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" }, "S"],
key: "ui.dialogs.shortcuts.automation_script.save",
key: "ui.dialogs.shortcuts.automations.paste",
},
],
},

View File

@@ -407,7 +407,6 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
align-items: center;
margin-right: 12px;
margin-inline-end: 12px;
margin-inline-start: initial;
}
`,
];

View File

@@ -36,7 +36,6 @@ export class HaVoiceCommandDialog extends LitElement {
@state() private _opened = false;
@state()
@storage({
key: "AssistPipelineId",
state: true,

View File

@@ -1,8 +1,6 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { property, state } from "lit/decorators";
import "@material/mwc-button";
import "../components/ha-spinner";
class HaInitPage extends LitElement {
@property({ type: Boolean }) public error = false;

View File

@@ -42,7 +42,6 @@ class PanelCalendar extends LitElement {
@state() private _error?: string = undefined;
@state()
@storage({
key: "deSelectedCalendars",
state: true,

View File

@@ -69,7 +69,6 @@ export class HaConfigApplicationCredentials extends LitElement {
})
private _activeHiddenColumns?: string[];
@state()
@storage({
storage: "sessionStorage",
key: "application-credentials-table-search",

View File

@@ -36,7 +36,6 @@ export default class HaAutomationAction extends LitElement {
@state() private _showReorder = false;
@state()
@storage({
key: "automationClipboard",
state: true,

View File

@@ -36,7 +36,6 @@ export default class HaAutomationCondition extends LitElement {
@state() private _showReorder = false;
@state()
@storage({
key: "automationClipboard",
state: true,

View File

@@ -18,7 +18,6 @@ import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { NumericStateCondition } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import { NON_NUMERIC_ATTRIBUTES } from "../../../../../data/entity_attributes";
const numericStateConditionStruct = object({
alias: optional(string()),
@@ -86,7 +85,72 @@ export default class HaNumericStateCondition extends LitElement {
name: "attribute",
selector: {
attribute: {
hide_attributes: NON_NUMERIC_ATTRIBUTES,
hide_attributes: [
"access_token",
"auto_update",
"available_modes",
"away_mode",
"changed_by",
"code_format",
"color_modes",
"current_activity",
"device_class",
"editable",
"effect_list",
"effect",
"entity_picture",
"event_type",
"event_types",
"fan_mode",
"fan_modes",
"fan_speed_list",
"forecast",
"friendly_name",
"frontend_stream_type",
"has_date",
"has_time",
"hs_color",
"hvac_mode",
"hvac_modes",
"icon",
"media_album_name",
"media_artist",
"media_content_type",
"media_position_updated_at",
"media_title",
"next_dawn",
"next_dusk",
"next_midnight",
"next_noon",
"next_rising",
"next_setting",
"operation_list",
"operation_mode",
"options",
"preset_mode",
"preset_modes",
"release_notes",
"release_summary",
"release_url",
"restored",
"rgb_color",
"rgbw_color",
"shuffle",
"sound_mode_list",
"sound_mode",
"source_list",
"source_type",
"source",
"state_class",
"supported_features",
"swing_mode",
"swing_mode",
"swing_modes",
"title",
"token",
"unit_of_measurement",
"xy_color",
],
},
},
context: {

View File

@@ -135,7 +135,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
@state() private _blueprintConfig?: BlueprintAutomationConfig;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
transformer: function (this: HaAutomationEditor, value) {

View File

@@ -138,7 +138,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
@state() private _filteredAutomations?: string[] | null;
@state()
@storage({
storage: "sessionStorage",
key: "automation-table-search",
@@ -147,7 +146,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
})
private _filter = "";
@state()
@storage({
storage: "sessionStorage",
key: "automation-table-filters-full",

View File

@@ -26,12 +26,8 @@ import type {
ManualAutomationConfig,
Trigger,
} from "../../../data/automation";
import {
isCondition,
isTrigger,
normalizeAutomationConfig,
} from "../../../data/automation";
import { getActionType, type Action } from "../../../data/script";
import { normalizeAutomationConfig } from "../../../data/automation";
import type { Action } from "../../../data/script";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
@@ -314,118 +310,54 @@ export class HaManualAutomationEditor extends LitElement {
return;
}
let loaded: any;
try {
loaded = load(paste);
} catch (_err: any) {
showToast(this, {
message: this.hass.localize(
"ui.panel.config.automation.editor.paste_invalid_yaml"
),
duration: 4000,
dismissable: true,
});
return;
}
const loaded: any = load(paste);
if (loaded) {
let normalized: AutomationConfig | undefined;
if (!loaded || typeof loaded !== "object") {
return;
}
let config = loaded;
if ("automation" in config) {
config = config.automation;
if (Array.isArray(config)) {
config = config[0];
try {
normalized = normalizeAutomationConfig(loaded);
} catch (_err: any) {
return;
}
}
if (Array.isArray(config)) {
if (config.length === 1) {
config = config[0];
} else {
const newConfig: AutomationConfig = {
triggers: [],
conditions: [],
actions: [],
};
let found = false;
config.forEach((cfg: any) => {
if (isTrigger(cfg)) {
found = true;
(newConfig.triggers as Trigger[]).push(cfg);
}
if (isCondition(cfg)) {
found = true;
(newConfig.conditions as Condition[]).push(cfg);
}
if (getActionType(cfg) !== "unknown") {
found = true;
(newConfig.actions as Action[]).push(cfg);
}
try {
assert(normalized, automationConfigStruct);
} catch (_err: any) {
showToast(this, {
message: this.hass.localize(
"ui.panel.config.automation.editor.paste_invalid_config"
),
duration: 4000,
dismissable: true,
});
if (found) {
config = newConfig;
}
return;
}
}
if (isTrigger(config)) {
config = { triggers: [config] };
}
if (isCondition(config)) {
config = { conditions: [config] };
}
if (getActionType(config) !== "unknown") {
config = { actions: [config] };
}
if (normalized) {
ev.preventDefault();
let normalized: AutomationConfig;
try {
normalized = normalizeAutomationConfig(config);
} catch (_err: any) {
return;
}
try {
assert(normalized, automationConfigStruct);
} catch (_err: any) {
showToast(this, {
message: this.hass.localize(
"ui.panel.config.automation.editor.paste_invalid_config"
),
duration: 4000,
dismissable: true,
});
return;
}
if (normalized) {
ev.preventDefault();
if (this.dirty) {
const result = await new Promise<boolean>((resolve) => {
showPasteReplaceDialog(this, {
domain: "automation",
pastedConfig: normalized,
onClose: () => resolve(false),
onAppend: () => {
this._appendToExistingConfig(normalized);
resolve(false);
},
onReplace: () => resolve(true),
if (this.dirty) {
const result = await new Promise<boolean>((resolve) => {
showPasteReplaceDialog(this, {
domain: "automation",
pastedConfig: normalized,
onClose: () => resolve(false),
onAppend: () => {
this._appendToExistingConfig(normalized);
resolve(false);
},
onReplace: () => resolve(true),
});
});
});
if (!result) {
return;
if (!result) {
return;
}
}
}
// replace the config completely
this._replaceExistingConfig(normalized);
// replace the config completely
this._replaceExistingConfig(normalized);
}
}
};

View File

@@ -29,7 +29,6 @@ export default class HaAutomationOption extends LitElement {
@state() private _showReorder = false;
@state()
@storage({
key: "automationClipboard",
state: true,

View File

@@ -38,7 +38,6 @@ export default class HaAutomationTrigger extends LitElement {
@state() private _showReorder = false;
@state()
@storage({
key: "automationClipboard",
state: true,

View File

@@ -1,6 +1,5 @@
import { mdiCog, mdiDelete, mdiHarddisk, mdiNas } from "@mdi/js";
import { css, html, LitElement, nothing, type TemplateResult } from "lit";
import { join } from "lit/directives/join";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
@@ -58,51 +57,26 @@ class HaBackupConfigAgents extends LitElement {
);
}
const texts: (TemplateResult | string)[] = [];
if (isNetworkMountAgent(agentId)) {
texts.push(
this.hass.localize(
"ui.panel.config.backup.agents.network_mount_agent_description"
)
);
}
const encryptionTurnedOff =
this.agentsConfig?.[agentId]?.protected === false;
if (encryptionTurnedOff) {
texts.push(
html`<div class="unencrypted-warning">
<span class="dot warning"></span>
<span>
${this.hass.localize(
"ui.panel.config.backup.agents.encryption_turned_off"
)}
</span>
</div>`
return html`
<span class="dot warning"></span>
<span>
${this.hass.localize(
"ui.panel.config.backup.agents.encryption_turned_off"
)}
</span>
`;
}
if (isNetworkMountAgent(agentId)) {
return this.hass.localize(
"ui.panel.config.backup.agents.network_mount_agent_description"
);
}
const retention = this.agentsConfig?.[agentId]?.retention;
if (retention) {
if (retention.copies === null && retention.days === null) {
texts.push(
this.hass.localize("ui.panel.config.backup.agents.retention_all")
);
} else {
texts.push(
this.hass.localize(
`ui.panel.config.backup.agents.retention_${retention.copies ? "backups" : "days"}`,
{
count: retention.copies || retention.days,
}
)
);
}
}
return join(texts, html`<span class="separator"> · </span>`);
return "";
}
private _availableAgents = memoizeOne(
@@ -313,11 +287,6 @@ class HaBackupConfigAgents extends LitElement {
gap: 8px;
line-height: normal;
}
.unencrypted-warning {
display: flex;
align-items: center;
gap: 4px;
}
.dot {
display: block;
position: relative;
@@ -325,22 +294,11 @@ class HaBackupConfigAgents extends LitElement {
height: 8px;
background-color: var(--disabled-color);
border-radius: 50%;
flex: none;
}
.dot.warning {
background-color: var(--warning-color);
}
@media all and (max-width: 500px) {
.separator {
display: none;
}
ha-md-list-item [slot="supporting-text"] {
display: flex;
align-items: flex-start;
flex-direction: column;
justify-content: flex-start;
gap: 4px;
}
}
`;
}

View File

@@ -98,7 +98,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
@state() private _selected: string[] = [];
@state()
@storage({
storage: "sessionStorage",
key: "backups-table-filters",

View File

@@ -118,17 +118,19 @@ class HaConfigBackupDetails extends LitElement {
</p>
</div>
`
: html`<ha-backup-config-retention
location-specific
.headline=${this.hass.localize(
`ui.panel.config.backup.location.retention_for_${isLocalAgent(this.agentId) ? "this_system" : "location"}`,
{ location: agentName }
)}
.hass=${this.hass}
.retention=${this.config?.agents[this.agentId]
?.retention}
@value-changed=${this._retentionChanged}
></ha-backup-config-retention>`}
: this.config?.agents[this.agentId]
? html`<ha-backup-config-retention
location-specific
.headline=${this.hass.localize(
`ui.panel.config.backup.location.retention_for_${isLocalAgent(this.agentId) ? "this_system" : "location"}`,
{ location: agentName }
)}
.hass=${this.hass}
.retention=${this.config?.agents[this.agentId]
?.retention}
@value-changed=${this._retentionChanged}
></ha-backup-config-retention>`
: nothing}
</ha-card>
<ha-card>
<div class="card-header">

View File

@@ -9,7 +9,7 @@ import {
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -118,7 +118,6 @@ class HaBlueprintOverview extends LitElement {
})
private _activeHiddenColumns?: string[];
@state()
@storage({
storage: "sessionStorage",
key: "blueprint-table-search",
@@ -500,11 +499,9 @@ class HaBlueprintOverview extends LitElement {
list: html`<ul>
${[...(related.automation || []), ...(related.script || [])].map(
(item) => {
const automationState = this.hass.states[item];
const state = this.hass.states[item];
return html`<li>
${automationState
? `${computeStateName(automationState)} (${item})`
: item}
${state ? `${computeStateName(state)} (${item})` : item}
</li>`;
}
)}

View File

@@ -106,7 +106,7 @@ export class HaDeviceCard extends LitElement {
<div class="extra-info">
${type === "bluetooth" &&
isComponentLoaded(this.hass, "bluetooth")
? html`${titleCase(type)}:
? html`${titleCase(type)}
<a
href="/config/bluetooth/advertisement-monitor?${createSearchParam(
{ address: value }
@@ -114,7 +114,7 @@ export class HaDeviceCard extends LitElement {
>${value.toUpperCase()}</a
>`
: type === "mac" && isComponentLoaded(this.hass, "dhcp")
? html`MAC:
? html`${titleCase(type)}
<a
href="/config/dhcp?${createSearchParam({
mac_address: value,

View File

@@ -1559,7 +1559,6 @@ export class HaConfigDevicePage extends LitElement {
align-items: center;
padding-left: 8px;
padding-inline-start: 8px;
padding-inline-end: initial;
direction: var(--direction);
}

View File

@@ -120,7 +120,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
@state() private _selected: string[] = [];
@state()
@storage({
storage: "sessionStorage",
key: "devices-table-search",
@@ -129,7 +128,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
})
private _filter: string = history.state?.filter || "";
@state()
@storage({
storage: "sessionStorage",
key: "devices-table-filters-full",

View File

@@ -159,7 +159,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
@consume({ context: fullEntitiesContext, subscribe: true })
_entities!: EntityRegistryEntry[];
@state()
@storage({
storage: "sessionStorage",
key: "entities-table-search",
@@ -170,7 +169,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
@state() private _searchParms = new URLSearchParams(window.location.search);
@state()
@storage({
storage: "sessionStorage",
key: "entities-table-filters",

View File

@@ -168,7 +168,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
})
private _activeCollapsed?: string;
@state()
@storage({
storage: "sessionStorage",
key: "helpers-table-search",

View File

@@ -156,7 +156,7 @@ class HaConfigInfo extends LitElement {
)}
</span>
<span class="version">
${JS_VERSION}${JS_TYPE !== "modern" ? ` · ${JS_TYPE}` : ""}
${JS_VERSION}${JS_TYPE !== "modern" ? ` ${JS_TYPE}` : ""}
</span>
</li>
</ul>

View File

@@ -210,9 +210,6 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement {
.route=${this.route}
.columns=${this._columns(this.hass.localize)}
.data=${this._dataWithNamedSourceAndIds(this._data)}
.noDataText=${this.hass.localize(
"ui.panel.config.bluetooth.no_advertisements_found"
)}
@row-click=${this._handleRowClicked}
.initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed}

View File

@@ -96,9 +96,6 @@ export class DHCPConfigPanel extends SubscribeMixin(LitElement) {
.route=${this.route}
.columns=${this._columns(this.hass.localize)}
.data=${this._dataWithIds(this._data)}
.noDataText=${this.hass.localize(
"ui.panel.config.dhcp.no_devices_found"
)}
filter=${this._macAddress || ""}
></hass-tabs-subpage-data-table>
`;

View File

@@ -1,7 +1,7 @@
import "@material/mwc-button";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { storage } from "../../../../../common/decorators/storage";
import "../../../../../components/ha-card";
import "../../../../../components/ha-code-editor";
@@ -23,7 +23,6 @@ export class MQTTConfigPanel extends LitElement {
@property({ type: Boolean }) public narrow = false;
@state()
@storage({
key: "panel-dev-mqtt-topic-ls",
state: true,
@@ -31,7 +30,6 @@ export class MQTTConfigPanel extends LitElement {
})
private _topic = "";
@state()
@storage({
key: "panel-dev-mqtt-payload-ls",
state: true,
@@ -39,7 +37,6 @@ export class MQTTConfigPanel extends LitElement {
})
private _payload = "";
@state()
@storage({
key: "panel-dev-mqtt-qos-ls",
state: true,
@@ -47,7 +44,6 @@ export class MQTTConfigPanel extends LitElement {
})
private _qos = "0";
@state()
@storage({
key: "panel-dev-mqtt-retain-ls",
state: true,
@@ -55,7 +51,6 @@ export class MQTTConfigPanel extends LitElement {
})
private _retain = false;
@state()
@storage({
key: "panel-dev-mqtt-allow-template-ls",
state: true,

View File

@@ -21,7 +21,6 @@ const qosLevel = ["0", "1", "2"];
class MqttSubscribeCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@storage({
key: "panel-dev-mqtt-topic-subscribe",
state: true,
@@ -29,7 +28,6 @@ class MqttSubscribeCard extends LitElement {
})
private _topic = "";
@state()
@storage({
key: "panel-dev-mqtt-qos-subscribe",
state: true,
@@ -37,7 +35,6 @@ class MqttSubscribeCard extends LitElement {
})
private _qos = "0";
@state()
@storage({
key: "panel-dev-mqtt-json-format",
state: true,

View File

@@ -54,8 +54,6 @@ class DialogSSDPDiscoveryInfo extends LitElement implements HassDialog {
)}
>
<p>
<b>${this.hass.localize("ui.panel.config.ssdp.name")}</b>:
${this._params.entry.name} <br />
<b>${this.hass.localize("ui.panel.config.ssdp.ssdp_st")}</b>:
${this._params.entry.ssdp_st} <br />
<b>${this.hass.localize("ui.panel.config.ssdp.ssdp_location")}</b>:

View File

@@ -57,21 +57,14 @@ export class SSDPConfigPanel extends SubscribeMixin(LitElement) {
private _columns = memoizeOne(
(localize: LocalizeFunc): DataTableColumnContainer => {
const columns: DataTableColumnContainer<SSDPDiscoveryData> = {
name: {
title: localize("ui.panel.config.ssdp.name"),
ssdp_st: {
title: localize("ui.panel.config.ssdp.ssdp_st"),
sortable: true,
filterable: true,
showNarrow: true,
main: true,
hideable: false,
moveable: false,
},
ssdp_st: {
title: localize("ui.panel.config.ssdp.ssdp_st"),
sortable: true,
filterable: true,
showNarrow: true,
hideable: false,
direction: "asc",
},
ssdp_location: {
@@ -105,9 +98,6 @@ export class SSDPConfigPanel extends SubscribeMixin(LitElement) {
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
.data=${this._dataWithIds(this._data)}
.noDataText=${this.hass.localize(
"ui.panel.config.ssdp.no_devices_found"
)}
@row-click=${this._handleRowClicked}
clickable
></hass-tabs-subpage-data-table>

View File

@@ -112,9 +112,6 @@ export class ZeroconfConfigPanel extends SubscribeMixin(LitElement) {
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
.data=${this._dataWithIds(this._data)}
.noDataText=${this.hass.localize(
"ui.panel.config.zeroconf.no_devices_found"
)}
@row-click=${this._handleRowClicked}
clickable
></hass-tabs-subpage-data-table>

View File

@@ -55,7 +55,6 @@ export class HaConfigLabels extends LitElement {
@state() private _labels: LabelRegistryEntry[] = [];
@state()
@storage({
storage: "sessionStorage",
key: "labels-table-search",

View File

@@ -70,7 +70,7 @@ class DownloadLogsDialog extends LitElement {
<span slot="subtitle">
${this._dialogParams.header}${this._dialogParams.boot === 0
? ""
: ` · ${this._dialogParams.boot === -1 ? this.hass.localize("ui.panel.config.logs.previous") : this.hass.localize("ui.panel.config.logs.startups_ago", { boot: this._dialogParams.boot * -1 })}`}
: ` ${this._dialogParams.boot === -1 ? this.hass.localize("ui.panel.config.logs.previous") : this.hass.localize("ui.panel.config.logs.startups_ago", { boot: this._dialogParams.boot * -1 })}`}
</span>
</ha-dialog-header>
<div slot="content" class="content">

View File

@@ -74,7 +74,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
@state() private _dashboards: LovelaceDashboard[] = [];
@state()
@storage({
storage: "sessionStorage",
key: "lovelace-dashboards-table-search",
@@ -162,7 +161,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
placement="right"
>
<ha-svg-icon
style="padding-left: 10px; padding-inline-start: 10px; padding-inline-end: initial; direction: var(--direction);"
style="padding-left: 10px; padding-inline-start: 10px; direction: var(--direction);"
.path=${mdiCheckCircleOutline}
></ha-svg-icon>
</ha-tooltip>

View File

@@ -46,7 +46,6 @@ export class HaConfigLovelaceRescources extends LitElement {
@state() private _resources: LovelaceResource[] = [];
@state()
@storage({
storage: "sessionStorage",
key: "lovelace-resources-table-search",

View File

@@ -0,0 +1,66 @@
import "@material/mwc-button/mwc-button";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-button";
import "../../../components/ha-card";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
@customElement("ha-config-network-ssdp")
class ConfigNetworkSSDP extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
protected render() {
return html`
<ha-card
outlined
header=${this.hass.localize("ui.panel.config.network.discovery.ssdp")}
>
<div class="card-content">
<p>
${this.hass.localize("ui.panel.config.network.discovery.ssdp_info")}
</p>
</div>
<div class="card-actions">
<a
href="/config/ssdp"
aria-label=${this.hass.localize(
"ui.panel.config.network.discovery.ssdp_browser"
)}
>
<ha-button>
${this.hass.localize(
"ui.panel.config.network.discovery.ssdp_browser"
)}
</ha-button>
</a>
</div>
</ha-card>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
ha-settings-row {
padding: 0;
}
.card-actions {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
align-items: center;
}
`, // row-reverse so we tab first to "save"
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-network-ssdp": ConfigNetworkSSDP;
}
}

View File

@@ -0,0 +1,70 @@
import "@material/mwc-button/mwc-button";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-button";
import "../../../components/ha-card";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
@customElement("ha-config-network-zeroconf")
class ConfigNetworkZeroconf extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
protected render() {
return html`
<ha-card
outlined
header=${this.hass.localize(
"ui.panel.config.network.discovery.zeroconf"
)}
>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.network.discovery.zeroconf_info"
)}
</p>
</div>
<div class="card-actions">
<a
href="/config/zeroconf"
aria-label=${this.hass.localize(
"ui.panel.config.network.discovery.zeroconf_browser"
)}
>
<ha-button>
${this.hass.localize(
"ui.panel.config.network.discovery.zeroconf_browser"
)}
</ha-button>
</a>
</div>
</ha-card>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
ha-settings-row {
padding: 0;
}
.card-actions {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
align-items: center;
}
`, // row-reverse so we tab first to "save"
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-network-zeroconf": ConfigNetworkZeroconf;
}
}

View File

@@ -3,18 +3,14 @@ import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../layouts/hass-subpage";
import "../../../components/ha-card";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-icon-next";
import type { HomeAssistant, Route } from "../../../types";
import "./ha-config-network";
import "./ha-config-network-ssdp";
import "./ha-config-network-zeroconf";
import "./ha-config-url-form";
import "./supervisor-hostname";
import "./supervisor-network";
const NETWORK_BROWSERS = ["dhcp", "ssdp", "zeroconf"] as const;
@customElement("ha-config-section-network")
class HaConfigSectionNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -41,38 +37,15 @@ class HaConfigSectionNetwork extends LitElement {
: ""}
<ha-config-url-form .hass=${this.hass}></ha-config-url-form>
<ha-config-network .hass=${this.hass}></ha-config-network>
${NETWORK_BROWSERS.some((component) =>
isComponentLoaded(this.hass, component)
)
? html`
<ha-card
outlined
class="discovery-card"
header=${this.hass.localize(
"ui.panel.config.network.discovery.title"
)}
>
<ha-md-list>
${NETWORK_BROWSERS.map(
(domain) => html`
<ha-md-list-item type="link" href="/config/${domain}">
<div slot="headline">
${this.hass.localize(
`ui.panel.config.network.discovery.${domain}`
)}
</div>
<div slot="supporting-text">
${this.hass.localize(
`ui.panel.config.network.discovery.${domain}_info`
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
`
)}
</ha-md-list>
</ha-card>
`
${isComponentLoaded(this.hass, "ssdp")
? html`<ha-config-network-ssdp
.hass=${this.hass}
></ha-config-network-ssdp>`
: ""}
${isComponentLoaded(this.hass, "zeroconf")
? html`<ha-config-network-zeroconf
.hass=${this.hass}
></ha-config-network-zeroconf>`
: ""}
</div>
</hass-subpage>
@@ -89,15 +62,13 @@ class HaConfigSectionNetwork extends LitElement {
supervisor-network,
ha-config-url-form,
ha-config-network,
.discovery-card {
ha-config-network-ssdp,
ha-config-network-zeroconf {
display: block;
margin: 0 auto;
margin-bottom: 24px;
max-width: 600px;
}
.discovery-card ha-md-list {
padding-top: 0;
}
`;
}

View File

@@ -20,7 +20,7 @@ class DialogRepairsIssueSubtitle extends LitElement {
protected render() {
const domainName = domainToName(this.hass.localize, this.issue.domain);
const reportedBy = domainName
? ` · ${this.hass.localize("ui.panel.config.repairs.reported_by", {
? ` ${this.hass.localize("ui.panel.config.repairs.reported_by", {
integration: domainName,
})}`
: "";

View File

@@ -100,13 +100,13 @@ class HaConfigRepairs extends LitElement {
${(issue.severity === "critical" ||
issue.severity === "error") &&
issue.created
? " · "
? " "
: ""}
${createdBy
? html`<span .title=${createdBy}>${createdBy}</span>`
: nothing}
${issue.ignored
? ` · ${this.hass.localize(
? ` ${this.hass.localize(
"ui.panel.config.repairs.dialog.ignored_in_version_short",
{ version: issue.dismissed_version }
)}`

View File

@@ -133,7 +133,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
@state() private _filteredScenes?: string[] | null;
@state()
@storage({
storage: "sessionStorage",
key: "scene-table-search",
@@ -142,7 +141,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
})
private _filter = "";
@state()
@storage({
storage: "sessionStorage",
key: "scene-table-filters-full",

View File

@@ -105,7 +105,6 @@ export class HaScriptEditor extends SubscribeMixin(
@state() private _readOnly = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
transformer: function (this: HaScriptEditor, value) {

View File

@@ -138,7 +138,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
@state() private _filteredScripts?: string[] | null;
@state()
@storage({
storage: "sessionStorage",
key: "script-table-search",
@@ -147,7 +146,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
})
private _filter = "";
@state()
@storage({
storage: "sessionStorage",
key: "script-table-filters-full",

View File

@@ -24,11 +24,7 @@ import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-markdown";
import type { Action, Fields, ScriptConfig } from "../../../data/script";
import {
getActionType,
MODES,
normalizeScriptConfig,
} from "../../../data/script";
import { MODES, normalizeScriptConfig } from "../../../data/script";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
@@ -238,90 +234,54 @@ export class HaManualScriptEditor extends LitElement {
return;
}
let loaded: any;
try {
loaded = load(paste);
} catch (_err: any) {
showToast(this, {
message: this.hass.localize(
"ui.panel.config.script.editor.paste_invalid_config"
),
duration: 4000,
dismissable: true,
});
return;
}
const loaded: any = load(paste);
if (loaded) {
let normalized: ScriptConfig | undefined;
if (!loaded || typeof loaded !== "object") {
return;
}
let config = loaded;
if ("script" in config) {
config = config.script;
if (Object.keys(config).length) {
config = config[Object.keys(config)[0]];
try {
normalized = normalizeScriptConfig(loaded);
} catch (_err: any) {
return;
}
}
if (Array.isArray(config)) {
if (config.length === 1) {
config = config[0];
} else {
config = { sequence: config };
}
}
if (!["sequence", "unknown"].includes(getActionType(config))) {
config = { sequence: [config] };
}
let normalized: ScriptConfig | undefined;
try {
normalized = normalizeScriptConfig(config);
} catch (_err: any) {
return;
}
try {
assert(normalized, scriptConfigStruct);
} catch (_err: any) {
showToast(this, {
message: this.hass.localize(
"ui.panel.config.script.editor.paste_invalid_config"
),
duration: 4000,
dismissable: true,
});
return;
}
if (normalized) {
ev.preventDefault();
if (this.dirty) {
const result = await new Promise<boolean>((resolve) => {
showPasteReplaceDialog(this, {
domain: "script",
pastedConfig: normalized,
onClose: () => resolve(false),
onAppend: () => {
this._appendToExistingConfig(normalized);
resolve(false);
},
onReplace: () => resolve(true),
});
try {
assert(normalized, scriptConfigStruct);
} catch (_err: any) {
showToast(this, {
message: this.hass.localize(
"ui.panel.config.script.editor.paste_invalid_config"
),
duration: 4000,
dismissable: true,
});
if (!result) {
return;
}
return;
}
// replace the config completely
this._replaceExistingConfig(normalized);
if (normalized) {
ev.preventDefault();
if (this.dirty) {
const result = await new Promise<boolean>((resolve) => {
showPasteReplaceDialog(this, {
domain: "script",
pastedConfig: normalized,
onClose: () => resolve(false),
onAppend: () => {
this._appendToExistingConfig(normalized);
resolve(false);
},
onReplace: () => resolve(true),
});
});
if (!result) {
return;
}
}
// replace the config completely
this._replaceExistingConfig(normalized);
}
}
};

View File

@@ -62,7 +62,6 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
return this.hass.auth.external?.config.canWriteTag;
}
@state()
@storage({
storage: "sessionStorage",
key: "tags-table-search",

View File

@@ -76,7 +76,6 @@ export class VoiceAssistantsExpose extends LitElement {
@state() private _extEntities?: Record<string, ExtEntityRegistryEntry>;
@state()
@storage({
storage: "sessionStorage",
key: "voice-expose-table-search",

View File

@@ -52,7 +52,6 @@ class HaPanelDevAction extends LitElement {
private _yamlValid = true;
@state()
@storage({
key: "panel-dev-action-state-service-data",
state: true,
@@ -60,7 +59,6 @@ class HaPanelDevAction extends LitElement {
})
private _serviceData?: ServiceAction = { action: "", target: {}, data: {} };
@state()
@storage({
key: "panel-dev-action-state-yaml-mode",
state: true,

View File

@@ -33,7 +33,6 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
@state() supportedLanguages?: string[];
@state()
@storage({
key: "assist_debug_language",
state: true,

View File

@@ -62,7 +62,6 @@ class HaPanelDevState extends LitElement {
@state() private _validJSON = true;
@state()
@storage({
key: "devToolsShowAttributes",
state: true,

View File

@@ -22,14 +22,11 @@ import type {
DeviceConsumptionEnergyPreference,
} from "../../data/energy";
import {
computeConsumptionData,
getEnergyDataCollection,
getEnergyGasUnit,
getEnergyWaterUnit,
getSummedData,
} from "../../data/energy";
import { fileDownload } from "../../util/file_download";
import type { StatisticValue } from "../../data/recorder";
const ENERGY_LOVELACE_CONFIG: LovelaceConfig = {
views: [
@@ -180,20 +177,18 @@ class PanelEnergy extends LitElement {
const csv: string[] = [];
csv[0] = headers;
const processCsvRow = function (
id: string,
type: string,
unit: string,
data: StatisticValue[]
) {
const processStat = function (stat: string, type: string, unit: string) {
let n = 0;
const row: string[] = [];
row.push(id);
if (!stats[stat]) {
return;
}
row.push(stat);
row.push(type);
row.push(unit.normalize("NFKD"));
times.forEach((t) => {
if (n < data.length && data[n].start === t) {
row.push((data[n].change ?? "").toString());
if (n < stats[stat].length && stats[stat][n].start === t) {
row.push((stats[stat][n].change ?? "").toString());
n++;
} else {
row.push("");
@@ -202,14 +197,6 @@ class PanelEnergy extends LitElement {
csv.push(row.join(",") + "\n");
};
const processStat = function (stat: string, type: string, unit: string) {
if (!stats[stat]) {
return;
}
processCsvRow(stat, type, unit, stats[stat]);
};
const currency = this.hass.config.currency;
const printCategory = function (
@@ -348,99 +335,6 @@ class PanelEnergy extends LitElement {
printCategory("device_consumption", devices, electricUnit);
const { summedData, compareSummedData: _ } = getSummedData(
energyData.state
);
const { consumption, compareConsumption: __ } = computeConsumptionData(
summedData,
undefined
);
const processConsumptionData = function (
type: string,
unit: string,
data: Record<number, number>
) {
const data2: StatisticValue[] = [];
Object.entries(data).forEach(([t, value]) => {
data2.push({
start: Number(t),
end: NaN,
change: value,
});
});
processCsvRow("", type, unit, data2);
};
const hasSolar = !!solar_productions.length;
const hasBattery = !!battery_ins.length;
const hasGridReturn = !!grid_productions.length;
const hasGridSource = !!grid_consumptions.length;
if (hasGridSource) {
processConsumptionData(
"calculated_consumed_grid",
electricUnit,
consumption.used_grid
);
if (hasBattery) {
processConsumptionData(
"calculated_grid_to_battery",
electricUnit,
consumption.grid_to_battery
);
}
}
if (hasGridReturn && hasBattery) {
processConsumptionData(
"calculated_battery_to_grid",
electricUnit,
consumption.battery_to_grid
);
}
if (hasBattery) {
processConsumptionData(
"calculated_consumed_battery",
electricUnit,
consumption.used_battery
);
}
if (hasSolar) {
processConsumptionData(
"calculated_consumed_solar",
electricUnit,
consumption.used_solar
);
if (hasBattery) {
processConsumptionData(
"calculated_solar_to_battery",
electricUnit,
consumption.solar_to_battery
);
}
if (hasGridReturn) {
processConsumptionData(
"calculated_solar_to_grid",
electricUnit,
consumption.solar_to_grid
);
}
}
if (
(hasGridSource ? 1 : 0) + (hasSolar ? 1 : 0) + (hasBattery ? 1 : 0) >
1
) {
processConsumptionData(
"calculated_total_consumption",
electricUnit,
consumption.used_total
);
}
const blob = new Blob(csv, {
type: "text/csv",
});

View File

@@ -63,7 +63,6 @@ class HaPanelHistory extends LitElement {
@state() private _endDate: Date;
@state()
@storage({
key: "historyPickedValue",
state: true,

View File

@@ -39,7 +39,6 @@ export class HaPanelLogbook extends LitElement {
@state()
private _showBack?: boolean;
@state()
@storage({
key: "logbookPickedValue",
state: true,

Some files were not shown because too many files have changed in this diff Show More