Compare commits

..

2 Commits

Author SHA1 Message Date
Aidan Timson
11697f6627 Use default 2025-11-24 15:05:22 +00:00
Aidan Timson
648a4888a3 Use entity naming for helper names 2025-11-24 15:01:20 +00:00
99 changed files with 1949 additions and 5238 deletions

View File

@@ -84,7 +84,6 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
stat_consumption: "sensor.energy_boiler",
},
],
device_consumption_water: [],
})
);
hass.mockWS(

View File

@@ -28,13 +28,13 @@
"dependencies": {
"@babel/runtime": "7.28.4",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.20.0",
"@codemirror/autocomplete": "6.19.1",
"@codemirror/commands": "6.10.0",
"@codemirror/language": "6.11.3",
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.38.8",
"@codemirror/view": "6.38.6",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.2",
@@ -96,10 +96,10 @@
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "3.0.8",
"color-name": "2.1.0",
"barcode-detector": "3.0.6",
"color-name": "2.0.2",
"comlink": "4.4.2",
"core-js": "3.47.0",
"core-js": "3.46.0",
"cropperjs": "1.6.2",
"culori": "4.0.2",
"date-fns": "4.1.0",
@@ -111,8 +111,8 @@
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
"hls.js": "1.6.15",
"home-assistant-js-websocket": "9.6.0",
"hls.js": "1.6.14",
"home-assistant-js-websocket": "9.5.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "10.7.18",
"js-yaml": "4.1.1",
@@ -122,7 +122,7 @@
"lit": "3.3.1",
"lit-html": "3.3.1",
"luxon": "3.7.2",
"marked": "17.0.1",
"marked": "17.0.0",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",
@@ -153,12 +153,12 @@
"@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.5",
"@bundle-stats/plugin-webpack-filter": "4.21.6",
"@lokalise/node-api": "15.4.0",
"@lokalise/node-api": "15.3.1",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.3.11",
"@rspack/core": "1.6.4",
"@rsdoctor/rspack-plugin": "1.3.8",
"@rspack/core": "1.6.1",
"@rspack/dev-server": "1.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.22",
@@ -178,7 +178,7 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.13",
"@vitest/coverage-v8": "4.0.8",
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
@@ -201,25 +201,25 @@
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "27.2.0",
"jsdom": "27.1.0",
"jszip": "3.10.1",
"lint-staged": "16.2.7",
"lint-staged": "16.2.6",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
"map-stream": "0.0.7",
"pinst": "3.0.0",
"prettier": "3.6.2",
"rspack-manifest-plugin": "5.2.0",
"rspack-manifest-plugin": "5.1.0",
"serve": "14.2.5",
"sinon": "21.0.0",
"tar": "7.5.2",
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.47.0",
"typescript-eslint": "8.46.3",
"vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.13",
"vitest": "4.0.8",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"

View File

@@ -0,0 +1,32 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3969_57097)">
<path d="M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V4C20 6.20914 18.2091 8 16 8H4C1.79086 8 0 6.20914 0 4V4Z" fill="white" fill-opacity="0.48"/>
<path d="M0 20C0 15.5817 3.58172 12 8 12H68C72.4183 12 76 15.5817 76 20V36C76 40.4183 72.4183 44 68 44H8C3.58172 44 0 40.4183 0 36V20Z" fill="#1C1C1C"/>
<path d="M8 12.5H68C72.1421 12.5 75.5 15.8579 75.5 20V36C75.5 40.1421 72.1421 43.5 68 43.5H8C3.85786 43.5 0.5 40.1421 0.5 36V20C0.5 15.8579 3.85786 12.5 8 12.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M32.9844 27.0156C32.9844 26.0781 32.7031 25.2656 32.1406 24.5781C31.5781 23.8594 30.8594 23.375 29.9844 23.125V22C29.9844 21.4375 30.125 20.9375 30.4062 20.5C30.6875 20.0312 31.0469 19.6719 31.4844 19.4219C31.9531 19.1406 32.4531 19 32.9844 19H43.0156C43.5469 19 44.0312 19.1406 44.4688 19.4219C44.9375 19.6719 45.3125 20.0312 45.5938 20.5C45.875 20.9375 46.0156 21.4375 46.0156 22V23.125C45.1406 23.375 44.4219 23.8594 43.8594 24.5781C43.2969 25.2656 43.0156 26.0781 43.0156 27.0156V28.9844H32.9844V27.0156ZM47 25C47.5625 25 48.0312 25.2031 48.4062 25.6094C48.8125 25.9844 49.0156 26.4531 49.0156 27.0156V31.9844C49.0156 32.5469 48.875 33.0625 48.5938 33.5312C48.3125 33.9688 47.9375 34.3281 47.4688 34.6094C47.0312 34.8594 46.5469 34.9844 46.0156 34.9844V36.0156C46.0156 36.2656 45.9062 36.5 45.6875 36.7188C45.5 36.9062 45.2656 37 44.9844 37C44.7344 37 44.5 36.9062 44.2812 36.7188C44.0938 36.5 44 36.2656 44 36.0156V34.9844H32V36.0156C32 36.2656 31.8906 36.5 31.6719 36.7188C31.4844 36.9062 31.2656 37 31.0156 37C30.7344 37 30.4844 36.9062 30.2656 36.7188C30.0781 36.5 29.9844 36.2656 29.9844 36.0156V34.9844C29.4531 34.9844 28.9531 34.8594 28.4844 34.6094C28.0469 34.3281 27.6875 33.9688 27.4062 33.5312C27.125 33.0625 26.9844 32.5469 26.9844 31.9844V27.0156C26.9844 26.4531 27.1719 25.9844 27.5469 25.6094C27.9531 25.2031 28.4375 25 29 25C29.5625 25 30.0312 25.2031 30.4062 25.6094C30.8125 25.9844 31.0156 26.4531 31.0156 27.0156V31H44.9844V27.0156C44.9844 26.4531 45.1719 25.9844 45.5469 25.6094C45.9531 25.2031 46.4375 25 47 25Z" fill="#03A9F4"/>
<path d="M0 56C0 51.5817 3.58172 48 8 48H68C72.4183 48 76 51.5817 76 56V72C76 76.4183 72.4183 80 68 80H8C3.58172 80 0 76.4183 0 72V56Z" fill="#1C1C1C"/>
<path d="M8 48.5H68C72.1421 48.5 75.5 51.8579 75.5 56V72C75.5 76.1421 72.1421 79.5 68 79.5H8C3.85786 79.5 0.5 76.1421 0.5 72V56C0.5 51.8579 3.85786 48.5 8 48.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M44 61.9844H47.9844V64H46.0156V72.0156H29.9844V64H28.0156V61.9844H32C31.4375 61.9844 30.9531 61.7969 30.5469 61.4219C30.1719 61.0156 29.9844 60.5469 29.9844 60.0156V55.9844H35.9844V60.0156C35.9844 60.5469 35.7812 61.0156 35.375 61.4219C35 61.7969 34.5469 61.9844 34.0156 61.9844H41.9844V58.9844C41.9844 58.7344 41.8906 58.5156 41.7031 58.3281C41.5156 58.1094 41.2812 58 41 58C40.7188 58 40.4844 58.1094 40.2969 58.3281C40.1094 58.5156 40.0156 58.7344 40.0156 58.9844H38C38 58.4531 38.125 57.9688 38.375 57.5312C38.6562 57.0625 39.0156 56.6875 39.4531 56.4062C39.9219 56.125 40.4375 55.9844 41 55.9844C41.5625 55.9844 42.0625 56.125 42.5 56.4062C42.9688 56.6875 43.3281 57.0625 43.5781 57.5312C43.8594 57.9688 44 58.4531 44 58.9844V61.9844ZM38.9844 70V64H37.0156V70H38.9844Z" fill="#03A9F4"/>
<path d="M0 92C0 87.5817 3.58172 84 8 84H68C72.4183 84 76 87.5817 76 92V108C76 112.418 72.4183 116 68 116H8C3.58172 116 0 112.418 0 108V92Z" fill="#1C1C1C"/>
<path d="M8 84.5H68C72.1421 84.5 75.5 87.8579 75.5 92V108C75.5 112.142 72.1421 115.5 68 115.5H8C3.85786 115.5 0.5 112.142 0.5 108V92C0.5 87.8579 3.85786 84.5 8 84.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M44.9844 94.9844C46.0781 94.9844 47.0156 95.3906 47.7969 96.2031C48.6094 96.9844 49.0156 97.9219 49.0156 99.0156V108.016H47V105.016H29V108.016H26.9844V93.0156H29V102.016H37.0156V94.9844H44.9844ZM35.0938 100.094C34.5 100.688 33.7969 100.984 32.9844 100.984C32.1719 100.984 31.4688 100.688 30.875 100.094C30.2812 99.5 29.9844 98.7969 29.9844 97.9844C29.9844 97.1719 30.2812 96.4688 30.875 95.875C31.4688 95.2812 32.1719 94.9844 32.9844 94.9844C33.7969 94.9844 34.5 95.2812 35.0938 95.875C35.6875 96.4688 35.9844 97.1719 35.9844 97.9844C35.9844 98.7969 35.6875 99.5 35.0938 100.094Z" fill="#03A9F4"/>
<path d="M0 128C0 123.582 3.58172 120 8 120H68C72.4183 120 76 123.582 76 128V144C76 148.418 72.4183 152 68 152H8C3.58172 152 0 148.418 0 144V128Z" fill="#1C1C1C"/>
<path d="M8 120.5H68C72.1421 120.5 75.5 123.858 75.5 128V144C75.5 148.142 72.1421 151.5 68 151.5H8C3.85786 151.5 0.5 148.142 0.5 144V128C0.5 123.858 3.85786 120.5 8 120.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M46.0156 136.984H47.9844V142.984C47.9844 143.516 47.7812 143.984 47.375 144.391C47 144.797 46.5469 145 46.0156 145C46.0156 145.281 45.9062 145.516 45.6875 145.703C45.5 145.891 45.2656 145.984 44.9844 145.984H31.0156C30.7344 145.984 30.4844 145.891 30.2656 145.703C30.0781 145.516 29.9844 145.281 29.9844 145C29.4531 145 28.9844 144.797 28.5781 144.391C28.2031 143.984 28.0156 143.516 28.0156 142.984V136.984H31.0156V136.234C31.0156 135.641 31.2344 135.125 31.6719 134.688C32.1406 134.219 32.6719 133.984 33.2656 133.984C33.8906 133.984 34.4531 134.234 34.9531 134.734L36.3125 136.281C36.5 136.5 36.7812 136.734 37.1562 136.984H44V128.828C44 128.609 43.9219 128.422 43.7656 128.266C43.6094 128.078 43.4062 127.984 43.1562 127.984C42.9375 127.984 42.75 128.062 42.5938 128.219L41.3281 129.484C41.3906 129.734 41.4219 129.906 41.4219 130C41.4219 130.344 41.3125 130.703 41.0938 131.078L38.3281 128.312C38.7031 128.094 39.0625 127.984 39.4062 127.984C39.5625 127.984 39.7344 128.016 39.9219 128.078L41.1875 126.812C41.7188 126.281 42.375 126.016 43.1562 126.016C43.9375 126.016 44.6094 126.297 45.1719 126.859C45.7344 127.391 46.0156 128.047 46.0156 128.828V136.984ZM31.5781 132.438C31.2031 132.031 31.0156 131.547 31.0156 130.984C31.0156 130.422 31.2031 129.953 31.5781 129.578C31.9531 129.203 32.4219 129.016 32.9844 129.016C33.5469 129.016 34.0156 129.203 34.3906 129.578C34.7969 129.953 35 130.422 35 130.984C35 131.547 34.7969 132.031 34.3906 132.438C34.0156 132.812 33.5469 133 32.9844 133C32.4219 133 31.9531 132.812 31.5781 132.438Z" fill="#03A9F4"/>
<path d="M84 4C84 1.79086 85.7909 0 88 0H100C102.209 0 104 1.79086 104 4V4C104 6.20914 102.209 8 100 8H88C85.7909 8 84 6.20914 84 4V4Z" fill="white" fill-opacity="0.48"/>
<path d="M84 20C84 15.5817 87.5817 12 92 12H152C156.418 12 160 15.5817 160 20V36C160 40.4183 156.418 44 152 44H92C87.5817 44 84 40.4183 84 36V20Z" fill="#1C1C1C"/>
<path d="M92 12.5H152C156.142 12.5 159.5 15.8579 159.5 20V36C159.5 40.1421 156.142 43.5 152 43.5H92C87.8579 43.5 84.5 40.1421 84.5 36V20C84.5 15.8579 87.8579 12.5 92 12.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M131.984 30.0156C131.984 30.7656 131.797 31.4531 131.422 32.0781C131.047 32.6719 130.562 33.1406 129.969 33.4844C129.844 34.2031 129.5 34.8125 128.938 35.3125C128.406 35.7812 127.766 36.0156 127.016 36.0156C126.359 36.0156 125.766 35.8281 125.234 35.4531C124.734 35.0781 124.391 34.5938 124.203 34H119.797C119.609 34.5938 119.25 35.0781 118.719 35.4531C118.219 35.8281 117.641 36.0156 116.984 36.0156C116.234 36.0156 115.578 35.7812 115.016 35.3125C114.484 34.8125 114.156 34.2031 114.031 33.4844C113.438 33.1406 112.953 32.6719 112.578 32.0781C112.203 31.4531 112.016 30.7656 112.016 30.0156C112.016 29.1094 112.266 28.3125 112.766 27.625C113.297 26.9375 113.969 26.4688 114.781 26.2188L113 24.3906L112.719 24.7188C112.5 24.9062 112.25 25 111.969 25C111.719 25 111.5 24.9062 111.312 24.7188C111.094 24.5312 110.984 24.2969 110.984 24.0156C110.984 23.7344 111.094 23.5 111.312 23.3125L113.281 21.2969C113.469 21.1094 113.703 21.0156 113.984 21.0156C114.266 21.0156 114.5 21.1094 114.688 21.2969C114.906 21.4844 115.016 21.7188 115.016 22C115.016 22.2812 114.906 22.5156 114.688 22.7031L114.406 22.9844L115.812 24.3906L116.609 22.0469C116.797 21.4219 117.156 20.9219 117.688 20.5469C118.219 20.1719 118.797 19.9844 119.422 19.9844H124.578C125.203 19.9844 125.781 20.1719 126.312 20.5469C126.844 20.9219 127.203 21.4219 127.391 22.0469L128.75 26.0781C129.375 26.2031 129.922 26.4531 130.391 26.8281C130.891 27.2031 131.281 27.6719 131.562 28.2344C131.844 28.7656 131.984 29.3594 131.984 30.0156ZM116.984 34C117.266 34 117.5 33.9062 117.688 33.7188C117.906 33.5 118.016 33.2656 118.016 33.0156C118.016 32.7344 117.906 32.5 117.688 32.3125C117.5 32.0938 117.266 31.9844 116.984 31.9844C116.734 31.9844 116.5 32.0938 116.281 32.3125C116.094 32.5 116 32.7344 116 33.0156C116 33.2656 116.094 33.5 116.281 33.7188C116.5 33.9062 116.734 34 116.984 34ZM121.016 25.9844V22H119.422C118.953 22 118.641 22.2344 118.484 22.7031L117.406 25.9844H121.016ZM122.984 22V25.9844H126.594L125.516 22.7031C125.359 22.2344 125.047 22 124.578 22H122.984ZM127.016 34C127.266 34 127.484 33.9062 127.672 33.7188C127.891 33.5 128 33.2656 128 33.0156C128 32.7344 127.891 32.5 127.672 32.3125C127.484 32.0938 127.266 31.9844 127.016 31.9844C126.734 31.9844 126.484 32.0938 126.266 32.3125C126.078 32.5 125.984 32.7344 125.984 33.0156C125.984 33.2656 126.078 33.5 126.266 33.7188C126.484 33.9062 126.734 34 127.016 34Z" fill="#03A9F4"/>
<path d="M84 56C84 51.5817 87.5817 48 92 48H152C156.418 48 160 51.5817 160 56V72C160 76.4183 156.418 80 152 80H92C87.5817 80 84 76.4183 84 72V56Z" fill="#1C1C1C"/>
<path d="M92 48.5H152C156.142 48.5 159.5 51.8579 159.5 56V72C159.5 76.1421 156.142 79.5 152 79.5H92C87.8579 79.5 84.5 76.1421 84.5 72V56C84.5 51.8579 87.8579 48.5 92 48.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M128.984 58.9844C130.078 58.9844 131.016 59.3906 131.797 60.2031C132.609 60.9844 133.016 61.9219 133.016 63.0156V72.0156H131V69.0156H113V72.0156H110.984V57.0156H113V66.0156H121.016V58.9844H128.984ZM119.094 64.0938C118.5 64.6875 117.797 64.9844 116.984 64.9844C116.172 64.9844 115.469 64.6875 114.875 64.0938C114.281 63.5 113.984 62.7969 113.984 61.9844C113.984 61.1719 114.281 60.4688 114.875 59.875C115.469 59.2812 116.172 58.9844 116.984 58.9844C117.797 58.9844 118.5 59.2812 119.094 59.875C119.688 60.4688 119.984 61.1719 119.984 61.9844C119.984 62.7969 119.688 63.5 119.094 64.0938Z" fill="#03A9F4"/>
<path d="M84 92C84 87.5817 87.5817 84 92 84H152C156.418 84 160 87.5817 160 92V108C160 112.418 156.418 116 152 116H92C87.5817 116 84 112.418 84 108V92Z" fill="#1C1C1C"/>
<path d="M92 84.5H152C156.142 84.5 159.5 87.8579 159.5 92V108C159.5 112.142 156.142 115.5 152 115.5H92C87.8579 115.5 84.5 112.142 84.5 108V92C84.5 87.8579 87.8579 84.5 92 84.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M112.016 94H131.984V106H130.016V103.984H125.984V106H124.016V96.0156H113.984V106H112.016V94ZM130.016 96.0156H125.984V97.9844H130.016V96.0156ZM125.984 102.016H130.016V100H125.984V102.016Z" fill="#03A9F4"/>
</g>
<defs>
<clipPath id="clip0_3969_57097">
<rect width="160" height="160" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,76 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4744_40067)">
<path d="M0 6C0 2.68629 2.68629 0 6 0H28C31.3137 0 34 2.68629 34 6C34 9.31371 31.3137 12 28 12H6C2.68629 12 0 9.31371 0 6Z" fill="white" fill-opacity="0.48"/>
<path d="M0 28C0 23.5817 3.58172 20 8 20H42.6667C47.0849 20 50.6667 23.5817 50.6667 28V36C50.6667 40.4183 47.0849 44 42.6667 44H8.00001C3.58173 44 0 40.4183 0 36V28Z" fill="#1C1C1C"/>
<path d="M8 20.5H42.667C46.809 20.5002 50.167 23.858 50.167 28V36C50.167 40.142 46.809 43.4998 42.667 43.5H8C3.85787 43.5 0.5 40.1421 0.5 36V28C0.5 23.8579 3.85786 20.5 8 20.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M6 32C6 28.6863 8.68629 26 12 26C15.3137 26 18 28.6863 18 32C18 35.3137 15.3137 38 12 38C8.68629 38 6 35.3137 6 32Z" fill="white" fill-opacity="0.24"/>
<path d="M24 31C24 29.3431 25.3431 28 27 28H39.6667C41.3235 28 42.6667 29.3431 42.6667 31V33C42.6667 34.6569 41.3235 36 39.6667 36H27C25.3431 36 24 34.6569 24 33V31Z" fill="white" fill-opacity="0.24"/>
<path d="M54.6666 28C54.6666 23.5817 58.2483 20 62.6666 20H97.3333C101.752 20 105.333 23.5817 105.333 28V36C105.333 40.4183 101.752 44 97.3333 44H62.6666C58.2484 44 54.6666 40.4183 54.6666 36V28Z" fill="#1C1C1C"/>
<path d="M62.6666 20.5H97.3336C101.476 20.5002 104.834 23.858 104.834 28V36C104.834 40.142 101.476 43.4998 97.3336 43.5H62.6666C58.5245 43.5 55.1666 40.1421 55.1666 36V28C55.1666 23.8579 58.5245 20.5 62.6666 20.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M60.6666 32C60.6666 28.6863 63.3529 26 66.6666 26C69.9803 26 72.6666 28.6863 72.6666 32C72.6666 35.3137 69.9803 38 66.6666 38C63.3529 38 60.6666 35.3137 60.6666 32Z" fill="white" fill-opacity="0.24"/>
<path d="M78.6666 31C78.6666 29.3431 80.0098 28 81.6666 28H94.3333C95.9901 28 97.3333 29.3431 97.3333 31V33C97.3333 34.6569 95.9901 36 94.3333 36H81.6666C80.0098 36 78.6666 34.6569 78.6666 33V31Z" fill="white" fill-opacity="0.24"/>
<path d="M109.333 28C109.333 23.5817 112.915 20 117.333 20H152C156.418 20 160 23.5817 160 28V36C160 40.4183 156.418 44 152 44H117.333C112.915 44 109.333 40.4183 109.333 36V28Z" fill="#1C1C1C"/>
<path d="M117.333 20.5H152C156.142 20.5002 159.5 23.858 159.5 28V36C159.5 40.142 156.142 43.4998 152 43.5H117.333C113.191 43.5 109.833 40.1421 109.833 36V28C109.833 23.8579 113.191 20.5 117.333 20.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M115.333 32C115.333 28.6863 118.02 26 121.333 26C124.647 26 127.333 28.6863 127.333 32C127.333 35.3137 124.647 38 121.333 38C118.02 38 115.333 35.3137 115.333 32Z" fill="white" fill-opacity="0.24"/>
<path d="M133.333 31C133.333 29.3431 134.677 28 136.333 28H149C150.657 28 152 29.3431 152 31V33C152 34.6569 150.657 36 149 36H136.333C134.677 36 133.333 34.6569 133.333 33V31Z" fill="white" fill-opacity="0.24"/>
<path d="M0 56C0 53.7909 1.79086 52 4 52H29C31.2091 52 33 53.7909 33 56C33 58.2091 31.2091 60 29 60H4C1.79086 60 0 58.2091 0 56Z" fill="white" fill-opacity="0.48"/>
<path d="M0 72C0 67.5817 3.58172 64 8 64H29C33.4183 64 37 67.5817 37 72V96C37 100.418 33.4183 104 29 104H8C3.58172 104 0 100.418 0 96V72Z" fill="#1C1C1C"/>
<path d="M8 64.5H29C33.1421 64.5 36.5 67.8579 36.5 72V96C36.5 100.142 33.1421 103.5 29 103.5H8C3.85786 103.5 0.5 100.142 0.5 96V72C0.5 67.8579 3.85786 64.5 8 64.5Z" stroke="white" stroke-opacity="0.24"/>
<mask id="mask0_4744_40067" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="6" y="72" width="25" height="24">
<path d="M18.5 74C16.6435 74 14.863 74.7375 13.5503 76.0503C12.2375 77.363 11.5 79.1435 11.5 81C11.5 83.38 12.69 85.47 14.5 86.74V89C14.5 89.2652 14.6054 89.5196 14.7929 89.7071C14.9804 89.8946 15.2348 90 15.5 90H21.5C21.7652 90 22.0196 89.8946 22.2071 89.7071C22.3946 89.5196 22.5 89.2652 22.5 89V86.74C24.31 85.47 25.5 83.38 25.5 81C25.5 79.1435 24.7625 77.363 23.4497 76.0503C22.137 74.7375 20.3565 74 18.5 74ZM15.5 93C15.5 93.2652 15.6054 93.5196 15.7929 93.7071C15.9804 93.8946 16.2348 94 16.5 94H20.5C20.7652 94 21.0196 93.8946 21.2071 93.7071C21.3946 93.5196 21.5 93.2652 21.5 93V92H15.5V93Z" fill="black"/>
</mask>
<g mask="url(#mask0_4744_40067)">
<rect x="6.5" y="72" width="24" height="24" fill="#03A9F4"/>
</g>
<path d="M41 72C41 67.5817 44.5817 64 49 64H70C74.4183 64 78 67.5817 78 72V96C78 100.418 74.4183 104 70 104H49C44.5817 104 41 100.418 41 96V72Z" fill="#1C1C1C"/>
<path d="M49 64.5H70C74.1421 64.5 77.5 67.8579 77.5 72V96C77.5 100.142 74.1421 103.5 70 103.5H49C44.8579 103.5 41.5 100.142 41.5 96V72C41.5 67.8579 44.8579 64.5 49 64.5Z" stroke="white" stroke-opacity="0.24"/>
<mask id="mask1_4744_40067" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="47" y="72" width="25" height="24">
<path d="M66.5 80C67.61 80 68.5 80.9 68.5 82V88.76C69.11 89.31 69.5 90.11 69.5 91C69.5 92.66 68.16 94 66.5 94C64.84 94 63.5 92.66 63.5 91C63.5 90.11 63.89 89.31 64.5 88.76V82C64.5 80.9 65.4 80 66.5 80ZM66.5 81C65.95 81 65.5 81.45 65.5 82V83H67.5V82C67.5 81.45 67.05 81 66.5 81ZM52.5 92V84H49.5L59.5 75L63.9 78.96C63.04 79.69 62.5 80.78 62.5 82V88C61.87 88.83 61.5 89.87 61.5 91L61.6 92H52.5Z" fill="black"/>
</mask>
<g mask="url(#mask1_4744_40067)">
<rect x="47.5" y="72" width="24" height="24" fill="#03A9F4"/>
</g>
<path d="M82 72C82 67.5817 85.5817 64 90 64H111C115.418 64 119 67.5817 119 72V96C119 100.418 115.418 104 111 104H90C85.5817 104 82 100.418 82 96V72Z" fill="#1C1C1C"/>
<path d="M90 64.5H111C115.142 64.5 118.5 67.8579 118.5 72V96C118.5 100.142 115.142 103.5 111 103.5H90C85.8579 103.5 82.5 100.142 82.5 96V72C82.5 67.8579 85.8579 64.5 90 64.5Z" stroke="white" stroke-opacity="0.24"/>
<mask id="mask2_4744_40067" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="88" y="72" width="25" height="24">
<path d="M100.5 84H107.5C106.97 88.11 104.22 91.78 100.5 92.92V84H93.5V78.3L100.5 75.19M100.5 73L91.5 77V83C91.5 88.55 95.34 93.73 100.5 95C105.66 93.73 109.5 88.55 109.5 83V77L100.5 73Z" fill="black"/>
</mask>
<g mask="url(#mask2_4744_40067)">
<rect x="88.5" y="72" width="24" height="24" fill="#03A9F4"/>
</g>
<path d="M123 72C123 67.5817 126.582 64 131 64H152C156.418 64 160 67.5817 160 72V96C160 100.418 156.418 104 152 104H131C126.582 104 123 100.418 123 96V72Z" fill="#1C1C1C"/>
<path d="M131 64.5H152C156.142 64.5 159.5 67.8579 159.5 72V96C159.5 100.142 156.142 103.5 152 103.5H131C126.858 103.5 123.5 100.142 123.5 96V72C123.5 67.8579 126.858 64.5 131 64.5Z" stroke="white" stroke-opacity="0.24"/>
<mask id="mask3_4744_40067" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="129" y="72" width="25" height="24">
<path d="M145.5 84C145.5 83.4696 145.711 82.9609 146.086 82.5858C146.461 82.2107 146.97 82 147.5 82C148.03 82 148.539 82.2107 148.914 82.5858C149.289 82.9609 149.5 83.4696 149.5 84C149.5 84.5304 149.289 85.0391 148.914 85.4142C148.539 85.7893 148.03 86 147.5 86C146.97 86 146.461 85.7893 146.086 85.4142C145.711 85.0391 145.5 84.5304 145.5 84ZM139.5 84C139.5 83.4696 139.711 82.9609 140.086 82.5858C140.461 82.2107 140.97 82 141.5 82C142.03 82 142.539 82.2107 142.914 82.5858C143.289 82.9609 143.5 83.4696 143.5 84C143.5 84.5304 143.289 85.0391 142.914 85.4142C142.539 85.7893 142.03 86 141.5 86C140.97 86 140.461 85.7893 140.086 85.4142C139.711 85.0391 139.5 84.5304 139.5 84ZM133.5 84C133.5 83.4696 133.711 82.9609 134.086 82.5858C134.461 82.2107 134.97 82 135.5 82C136.03 82 136.539 82.2107 136.914 82.5858C137.289 82.9609 137.5 83.4696 137.5 84C137.5 84.5304 137.289 85.0391 136.914 85.4142C136.539 85.7893 136.03 86 135.5 86C134.97 86 134.461 85.7893 134.086 85.4142C133.711 85.0391 133.5 84.5304 133.5 84Z" fill="black"/>
</mask>
<g mask="url(#mask3_4744_40067)">
<rect x="129.5" y="72" width="24" height="24" fill="#03A9F4"/>
</g>
<path d="M0 116C0 113.791 1.79086 112 4 112H29C31.2091 112 33 113.791 33 116C33 118.209 31.2091 120 29 120H4C1.79086 120 0 118.209 0 116Z" fill="white" fill-opacity="0.48"/>
<path d="M0 132C0 127.582 3.58172 124 8 124H70C74.4183 124 78 127.582 78 132V160H0V132Z" fill="url(#paint0_linear_4744_40067)"/>
<path d="M8 124.5H70C74.1421 124.5 77.5 127.858 77.5 132V159.5H0.5V132C0.5 127.858 3.85786 124.5 8 124.5Z" stroke="url(#paint1_linear_4744_40067)" stroke-opacity="0.12"/>
<path d="M82 132C82 127.582 85.5817 124 90 124H152C156.418 124 160 127.582 160 132V160H82V132Z" fill="url(#paint2_linear_4744_40067)"/>
<path d="M90 124.5H152C156.142 124.5 159.5 127.858 159.5 132V159.5H82.5V132C82.5 127.858 85.8579 124.5 90 124.5Z" stroke="url(#paint3_linear_4744_40067)" stroke-opacity="0.12"/>
</g>
<defs>
<linearGradient id="paint0_linear_4744_40067" x1="39" y1="124" x2="39" y2="160" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-color="#1C1C1C"/>
<stop offset="1" stop-color="#1C1C1C" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_4744_40067" x1="39" y1="124" x2="39" y2="160" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-color="white" stop-opacity="0.24"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint2_linear_4744_40067" x1="121" y1="124" x2="121" y2="160" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-color="#1C1C1C"/>
<stop offset="1" stop-color="#1C1C1C" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint3_linear_4744_40067" x1="121" y1="124" x2="121" y2="160" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-color="white" stop-opacity="0.24"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<clipPath id="clip0_4744_40067">
<rect width="160" height="160" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1,32 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3969_50764)">
<path d="M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V4C20 6.20914 18.2091 8 16 8H4C1.79086 8 0 6.20914 0 4V4Z" fill="black" fill-opacity="0.32"/>
<path d="M0 20C0 15.5817 3.58172 12 8 12H68C72.4183 12 76 15.5817 76 20V36C76 40.4183 72.4183 44 68 44H8C3.58172 44 0 40.4183 0 36V20Z" fill="white"/>
<path d="M8 12.5H68C72.1421 12.5 75.5 15.8579 75.5 20V36C75.5 40.1421 72.1421 43.5 68 43.5H8C3.85786 43.5 0.5 40.1421 0.5 36V20C0.5 15.8579 3.85786 12.5 8 12.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M32.9844 27.0156C32.9844 26.0781 32.7031 25.2656 32.1406 24.5781C31.5781 23.8594 30.8594 23.375 29.9844 23.125V22C29.9844 21.4375 30.125 20.9375 30.4062 20.5C30.6875 20.0312 31.0469 19.6719 31.4844 19.4219C31.9531 19.1406 32.4531 19 32.9844 19H43.0156C43.5469 19 44.0312 19.1406 44.4688 19.4219C44.9375 19.6719 45.3125 20.0312 45.5938 20.5C45.875 20.9375 46.0156 21.4375 46.0156 22V23.125C45.1406 23.375 44.4219 23.8594 43.8594 24.5781C43.2969 25.2656 43.0156 26.0781 43.0156 27.0156V28.9844H32.9844V27.0156ZM47 25C47.5625 25 48.0312 25.2031 48.4062 25.6094C48.8125 25.9844 49.0156 26.4531 49.0156 27.0156V31.9844C49.0156 32.5469 48.875 33.0625 48.5938 33.5312C48.3125 33.9688 47.9375 34.3281 47.4688 34.6094C47.0312 34.8594 46.5469 34.9844 46.0156 34.9844V36.0156C46.0156 36.2656 45.9062 36.5 45.6875 36.7188C45.5 36.9062 45.2656 37 44.9844 37C44.7344 37 44.5 36.9062 44.2812 36.7188C44.0938 36.5 44 36.2656 44 36.0156V34.9844H32V36.0156C32 36.2656 31.8906 36.5 31.6719 36.7188C31.4844 36.9062 31.2656 37 31.0156 37C30.7344 37 30.4844 36.9062 30.2656 36.7188C30.0781 36.5 29.9844 36.2656 29.9844 36.0156V34.9844C29.4531 34.9844 28.9531 34.8594 28.4844 34.6094C28.0469 34.3281 27.6875 33.9688 27.4062 33.5312C27.125 33.0625 26.9844 32.5469 26.9844 31.9844V27.0156C26.9844 26.4531 27.1719 25.9844 27.5469 25.6094C27.9531 25.2031 28.4375 25 29 25C29.5625 25 30.0312 25.2031 30.4062 25.6094C30.8125 25.9844 31.0156 26.4531 31.0156 27.0156V31H44.9844V27.0156C44.9844 26.4531 45.1719 25.9844 45.5469 25.6094C45.9531 25.2031 46.4375 25 47 25Z" fill="#03A9F4"/>
<path d="M0 56C0 51.5817 3.58172 48 8 48H68C72.4183 48 76 51.5817 76 56V72C76 76.4183 72.4183 80 68 80H8C3.58172 80 0 76.4183 0 72V56Z" fill="white"/>
<path d="M8 48.5H68C72.1421 48.5 75.5 51.8579 75.5 56V72C75.5 76.1421 72.1421 79.5 68 79.5H8C3.85786 79.5 0.5 76.1421 0.5 72V56C0.5 51.8579 3.85786 48.5 8 48.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M44 61.9844H47.9844V64H46.0156V72.0156H29.9844V64H28.0156V61.9844H32C31.4375 61.9844 30.9531 61.7969 30.5469 61.4219C30.1719 61.0156 29.9844 60.5469 29.9844 60.0156V55.9844H35.9844V60.0156C35.9844 60.5469 35.7812 61.0156 35.375 61.4219C35 61.7969 34.5469 61.9844 34.0156 61.9844H41.9844V58.9844C41.9844 58.7344 41.8906 58.5156 41.7031 58.3281C41.5156 58.1094 41.2812 58 41 58C40.7188 58 40.4844 58.1094 40.2969 58.3281C40.1094 58.5156 40.0156 58.7344 40.0156 58.9844H38C38 58.4531 38.125 57.9688 38.375 57.5312C38.6562 57.0625 39.0156 56.6875 39.4531 56.4062C39.9219 56.125 40.4375 55.9844 41 55.9844C41.5625 55.9844 42.0625 56.125 42.5 56.4062C42.9688 56.6875 43.3281 57.0625 43.5781 57.5312C43.8594 57.9688 44 58.4531 44 58.9844V61.9844ZM38.9844 70V64H37.0156V70H38.9844Z" fill="#03A9F4"/>
<path d="M0 92C0 87.5817 3.58172 84 8 84H68C72.4183 84 76 87.5817 76 92V108C76 112.418 72.4183 116 68 116H8C3.58172 116 0 112.418 0 108V92Z" fill="white"/>
<path d="M8 84.5H68C72.1421 84.5 75.5 87.8579 75.5 92V108C75.5 112.142 72.1421 115.5 68 115.5H8C3.85786 115.5 0.5 112.142 0.5 108V92C0.5 87.8579 3.85786 84.5 8 84.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M44.9844 94.9844C46.0781 94.9844 47.0156 95.3906 47.7969 96.2031C48.6094 96.9844 49.0156 97.9219 49.0156 99.0156V108.016H47V105.016H29V108.016H26.9844V93.0156H29V102.016H37.0156V94.9844H44.9844ZM35.0938 100.094C34.5 100.688 33.7969 100.984 32.9844 100.984C32.1719 100.984 31.4688 100.688 30.875 100.094C30.2812 99.5 29.9844 98.7969 29.9844 97.9844C29.9844 97.1719 30.2812 96.4688 30.875 95.875C31.4688 95.2812 32.1719 94.9844 32.9844 94.9844C33.7969 94.9844 34.5 95.2812 35.0938 95.875C35.6875 96.4688 35.9844 97.1719 35.9844 97.9844C35.9844 98.7969 35.6875 99.5 35.0938 100.094Z" fill="#03A9F4"/>
<path d="M0 128C0 123.582 3.58172 120 8 120H68C72.4183 120 76 123.582 76 128V144C76 148.418 72.4183 152 68 152H8C3.58172 152 0 148.418 0 144V128Z" fill="white"/>
<path d="M8 120.5H68C72.1421 120.5 75.5 123.858 75.5 128V144C75.5 148.142 72.1421 151.5 68 151.5H8C3.85786 151.5 0.5 148.142 0.5 144V128C0.5 123.858 3.85786 120.5 8 120.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M46.0156 136.984H47.9844V142.984C47.9844 143.516 47.7812 143.984 47.375 144.391C47 144.797 46.5469 145 46.0156 145C46.0156 145.281 45.9062 145.516 45.6875 145.703C45.5 145.891 45.2656 145.984 44.9844 145.984H31.0156C30.7344 145.984 30.4844 145.891 30.2656 145.703C30.0781 145.516 29.9844 145.281 29.9844 145C29.4531 145 28.9844 144.797 28.5781 144.391C28.2031 143.984 28.0156 143.516 28.0156 142.984V136.984H31.0156V136.234C31.0156 135.641 31.2344 135.125 31.6719 134.688C32.1406 134.219 32.6719 133.984 33.2656 133.984C33.8906 133.984 34.4531 134.234 34.9531 134.734L36.3125 136.281C36.5 136.5 36.7812 136.734 37.1562 136.984H44V128.828C44 128.609 43.9219 128.422 43.7656 128.266C43.6094 128.078 43.4062 127.984 43.1562 127.984C42.9375 127.984 42.75 128.062 42.5938 128.219L41.3281 129.484C41.3906 129.734 41.4219 129.906 41.4219 130C41.4219 130.344 41.3125 130.703 41.0938 131.078L38.3281 128.312C38.7031 128.094 39.0625 127.984 39.4062 127.984C39.5625 127.984 39.7344 128.016 39.9219 128.078L41.1875 126.812C41.7188 126.281 42.375 126.016 43.1562 126.016C43.9375 126.016 44.6094 126.297 45.1719 126.859C45.7344 127.391 46.0156 128.047 46.0156 128.828V136.984ZM31.5781 132.438C31.2031 132.031 31.0156 131.547 31.0156 130.984C31.0156 130.422 31.2031 129.953 31.5781 129.578C31.9531 129.203 32.4219 129.016 32.9844 129.016C33.5469 129.016 34.0156 129.203 34.3906 129.578C34.7969 129.953 35 130.422 35 130.984C35 131.547 34.7969 132.031 34.3906 132.438C34.0156 132.812 33.5469 133 32.9844 133C32.4219 133 31.9531 132.812 31.5781 132.438Z" fill="#03A9F4"/>
<path d="M84 4C84 1.79086 85.7909 0 88 0H100C102.209 0 104 1.79086 104 4V4C104 6.20914 102.209 8 100 8H88C85.7909 8 84 6.20914 84 4V4Z" fill="black" fill-opacity="0.32"/>
<path d="M84 20C84 15.5817 87.5817 12 92 12H152C156.418 12 160 15.5817 160 20V36C160 40.4183 156.418 44 152 44H92C87.5817 44 84 40.4183 84 36V20Z" fill="white"/>
<path d="M92 12.5H152C156.142 12.5 159.5 15.8579 159.5 20V36C159.5 40.1421 156.142 43.5 152 43.5H92C87.8579 43.5 84.5 40.1421 84.5 36V20C84.5 15.8579 87.8579 12.5 92 12.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M131.984 30.0156C131.984 30.7656 131.797 31.4531 131.422 32.0781C131.047 32.6719 130.562 33.1406 129.969 33.4844C129.844 34.2031 129.5 34.8125 128.938 35.3125C128.406 35.7812 127.766 36.0156 127.016 36.0156C126.359 36.0156 125.766 35.8281 125.234 35.4531C124.734 35.0781 124.391 34.5938 124.203 34H119.797C119.609 34.5938 119.25 35.0781 118.719 35.4531C118.219 35.8281 117.641 36.0156 116.984 36.0156C116.234 36.0156 115.578 35.7812 115.016 35.3125C114.484 34.8125 114.156 34.2031 114.031 33.4844C113.438 33.1406 112.953 32.6719 112.578 32.0781C112.203 31.4531 112.016 30.7656 112.016 30.0156C112.016 29.1094 112.266 28.3125 112.766 27.625C113.297 26.9375 113.969 26.4688 114.781 26.2188L113 24.3906L112.719 24.7188C112.5 24.9062 112.25 25 111.969 25C111.719 25 111.5 24.9062 111.312 24.7188C111.094 24.5312 110.984 24.2969 110.984 24.0156C110.984 23.7344 111.094 23.5 111.312 23.3125L113.281 21.2969C113.469 21.1094 113.703 21.0156 113.984 21.0156C114.266 21.0156 114.5 21.1094 114.688 21.2969C114.906 21.4844 115.016 21.7188 115.016 22C115.016 22.2812 114.906 22.5156 114.688 22.7031L114.406 22.9844L115.812 24.3906L116.609 22.0469C116.797 21.4219 117.156 20.9219 117.688 20.5469C118.219 20.1719 118.797 19.9844 119.422 19.9844H124.578C125.203 19.9844 125.781 20.1719 126.312 20.5469C126.844 20.9219 127.203 21.4219 127.391 22.0469L128.75 26.0781C129.375 26.2031 129.922 26.4531 130.391 26.8281C130.891 27.2031 131.281 27.6719 131.562 28.2344C131.844 28.7656 131.984 29.3594 131.984 30.0156ZM116.984 34C117.266 34 117.5 33.9062 117.688 33.7188C117.906 33.5 118.016 33.2656 118.016 33.0156C118.016 32.7344 117.906 32.5 117.688 32.3125C117.5 32.0938 117.266 31.9844 116.984 31.9844C116.734 31.9844 116.5 32.0938 116.281 32.3125C116.094 32.5 116 32.7344 116 33.0156C116 33.2656 116.094 33.5 116.281 33.7188C116.5 33.9062 116.734 34 116.984 34ZM121.016 25.9844V22H119.422C118.953 22 118.641 22.2344 118.484 22.7031L117.406 25.9844H121.016ZM122.984 22V25.9844H126.594L125.516 22.7031C125.359 22.2344 125.047 22 124.578 22H122.984ZM127.016 34C127.266 34 127.484 33.9062 127.672 33.7188C127.891 33.5 128 33.2656 128 33.0156C128 32.7344 127.891 32.5 127.672 32.3125C127.484 32.0938 127.266 31.9844 127.016 31.9844C126.734 31.9844 126.484 32.0938 126.266 32.3125C126.078 32.5 125.984 32.7344 125.984 33.0156C125.984 33.2656 126.078 33.5 126.266 33.7188C126.484 33.9062 126.734 34 127.016 34Z" fill="#03A9F4"/>
<path d="M84 56C84 51.5817 87.5817 48 92 48H152C156.418 48 160 51.5817 160 56V72C160 76.4183 156.418 80 152 80H92C87.5817 80 84 76.4183 84 72V56Z" fill="white"/>
<path d="M92 48.5H152C156.142 48.5 159.5 51.8579 159.5 56V72C159.5 76.1421 156.142 79.5 152 79.5H92C87.8579 79.5 84.5 76.1421 84.5 72V56C84.5 51.8579 87.8579 48.5 92 48.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M128.984 58.9844C130.078 58.9844 131.016 59.3906 131.797 60.2031C132.609 60.9844 133.016 61.9219 133.016 63.0156V72.0156H131V69.0156H113V72.0156H110.984V57.0156H113V66.0156H121.016V58.9844H128.984ZM119.094 64.0938C118.5 64.6875 117.797 64.9844 116.984 64.9844C116.172 64.9844 115.469 64.6875 114.875 64.0938C114.281 63.5 113.984 62.7969 113.984 61.9844C113.984 61.1719 114.281 60.4688 114.875 59.875C115.469 59.2812 116.172 58.9844 116.984 58.9844C117.797 58.9844 118.5 59.2812 119.094 59.875C119.688 60.4688 119.984 61.1719 119.984 61.9844C119.984 62.7969 119.688 63.5 119.094 64.0938Z" fill="#03A9F4"/>
<path d="M84 92C84 87.5817 87.5817 84 92 84H152C156.418 84 160 87.5817 160 92V108C160 112.418 156.418 116 152 116H92C87.5817 116 84 112.418 84 108V92Z" fill="white"/>
<path d="M92 84.5H152C156.142 84.5 159.5 87.8579 159.5 92V108C159.5 112.142 156.142 115.5 152 115.5H92C87.8579 115.5 84.5 112.142 84.5 108V92C84.5 87.8579 87.8579 84.5 92 84.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M112.016 94H131.984V106H130.016V103.984H125.984V106H124.016V96.0156H113.984V106H112.016V94ZM130.016 96.0156H125.984V97.9844H130.016V96.0156ZM125.984 102.016H130.016V100H125.984V102.016Z" fill="#03A9F4"/>
</g>
<defs>
<clipPath id="clip0_3969_50764">
<rect width="160" height="160" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,76 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4744_39984)">
<path d="M0 6C0 2.68629 2.68629 0 6 0H28C31.3137 0 34 2.68629 34 6C34 9.31371 31.3137 12 28 12H6C2.68629 12 0 9.31371 0 6Z" fill="black" fill-opacity="0.32"/>
<path d="M0 28C0 23.5817 3.58172 20 8 20H42.6667C47.0849 20 50.6667 23.5817 50.6667 28V36C50.6667 40.4183 47.0849 44 42.6667 44H8.00001C3.58173 44 0 40.4183 0 36V28Z" fill="white"/>
<path d="M8 20.5H42.667C46.809 20.5002 50.167 23.858 50.167 28V36C50.167 40.142 46.809 43.4998 42.667 43.5H8C3.85787 43.5 0.5 40.1421 0.5 36V28C0.5 23.8579 3.85786 20.5 8 20.5Z" stroke="black" stroke-opacity="0.12"/>
<rect x="6" y="26" width="12" height="12" rx="6" fill="black" fill-opacity="0.12"/>
<path d="M24 31C24 29.3431 25.3431 28 27 28H39.6667C41.3235 28 42.6667 29.3431 42.6667 31V33C42.6667 34.6569 41.3235 36 39.6667 36H27C25.3431 36 24 34.6569 24 33V31Z" fill="black" fill-opacity="0.12"/>
<path d="M54.6667 28C54.6667 23.5817 58.2484 20 62.6667 20H97.3333C101.752 20 105.333 23.5817 105.333 28V36C105.333 40.4183 101.752 44 97.3334 44H62.6667C58.2484 44 54.6667 40.4183 54.6667 36V28Z" fill="white"/>
<path d="M62.6667 20.5H97.3337C101.476 20.5002 104.834 23.858 104.834 28V36C104.834 40.142 101.476 43.4998 97.3337 43.5H62.6667C58.5246 43.5 55.1667 40.1421 55.1667 36V28C55.1667 23.8579 58.5246 20.5 62.6667 20.5Z" stroke="black" stroke-opacity="0.12"/>
<rect x="60.6667" y="26" width="12" height="12" rx="6" fill="black" fill-opacity="0.12"/>
<path d="M78.6667 31C78.6667 29.3431 80.0098 28 81.6667 28H94.3334C95.9902 28 97.3334 29.3431 97.3334 31V33C97.3334 34.6569 95.9902 36 94.3334 36H81.6667C80.0098 36 78.6667 34.6569 78.6667 33V31Z" fill="black" fill-opacity="0.12"/>
<path d="M109.333 28C109.333 23.5817 112.915 20 117.333 20H152C156.418 20 160 23.5817 160 28V36C160 40.4183 156.418 44 152 44H117.333C112.915 44 109.333 40.4183 109.333 36V28Z" fill="white"/>
<path d="M117.333 20.5H152C156.142 20.5002 159.5 23.858 159.5 28V36C159.5 40.142 156.142 43.4998 152 43.5H117.333C113.191 43.5 109.833 40.1421 109.833 36V28C109.833 23.8579 113.191 20.5 117.333 20.5Z" stroke="black" stroke-opacity="0.12"/>
<rect x="115.333" y="26" width="12" height="12" rx="6" fill="black" fill-opacity="0.12"/>
<path d="M133.333 31C133.333 29.3431 134.676 28 136.333 28H149C150.657 28 152 29.3431 152 31V33C152 34.6569 150.657 36 149 36H136.333C134.676 36 133.333 34.6569 133.333 33V31Z" fill="black" fill-opacity="0.12"/>
<path d="M0 56C0 53.7909 1.79086 52 4 52H29C31.2091 52 33 53.7909 33 56C33 58.2091 31.2091 60 29 60H4C1.79086 60 0 58.2091 0 56Z" fill="black" fill-opacity="0.32"/>
<path d="M0 72C0 67.5817 3.58172 64 8 64H29C33.4183 64 37 67.5817 37 72V96C37 100.418 33.4183 104 29 104H8C3.58172 104 0 100.418 0 96V72Z" fill="white"/>
<path d="M8 64.5H29C33.1421 64.5 36.5 67.8579 36.5 72V96C36.5 100.142 33.1421 103.5 29 103.5H8C3.85786 103.5 0.5 100.142 0.5 96V72C0.5 67.8579 3.85786 64.5 8 64.5Z" stroke="black" stroke-opacity="0.12"/>
<mask id="mask0_4744_39984" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="6" y="72" width="25" height="24">
<path d="M18.5 74C16.6435 74 14.863 74.7375 13.5503 76.0503C12.2375 77.363 11.5 79.1435 11.5 81C11.5 83.38 12.69 85.47 14.5 86.74V89C14.5 89.2652 14.6054 89.5196 14.7929 89.7071C14.9804 89.8946 15.2348 90 15.5 90H21.5C21.7652 90 22.0196 89.8946 22.2071 89.7071C22.3946 89.5196 22.5 89.2652 22.5 89V86.74C24.31 85.47 25.5 83.38 25.5 81C25.5 79.1435 24.7625 77.363 23.4497 76.0503C22.137 74.7375 20.3565 74 18.5 74ZM15.5 93C15.5 93.2652 15.6054 93.5196 15.7929 93.7071C15.9804 93.8946 16.2348 94 16.5 94H20.5C20.7652 94 21.0196 93.8946 21.2071 93.7071C21.3946 93.5196 21.5 93.2652 21.5 93V92H15.5V93Z" fill="black"/>
</mask>
<g mask="url(#mask0_4744_39984)">
<rect x="6.5" y="72" width="24" height="24" fill="#03A9F4"/>
</g>
<path d="M41 72C41 67.5817 44.5817 64 49 64H70C74.4183 64 78 67.5817 78 72V96C78 100.418 74.4183 104 70 104H49C44.5817 104 41 100.418 41 96V72Z" fill="white"/>
<path d="M49 64.5H70C74.1421 64.5 77.5 67.8579 77.5 72V96C77.5 100.142 74.1421 103.5 70 103.5H49C44.8579 103.5 41.5 100.142 41.5 96V72C41.5 67.8579 44.8579 64.5 49 64.5Z" stroke="black" stroke-opacity="0.12"/>
<mask id="mask1_4744_39984" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="47" y="72" width="25" height="24">
<path d="M66.5 80C67.61 80 68.5 80.9 68.5 82V88.76C69.11 89.31 69.5 90.11 69.5 91C69.5 92.66 68.16 94 66.5 94C64.84 94 63.5 92.66 63.5 91C63.5 90.11 63.89 89.31 64.5 88.76V82C64.5 80.9 65.4 80 66.5 80ZM66.5 81C65.95 81 65.5 81.45 65.5 82V83H67.5V82C67.5 81.45 67.05 81 66.5 81ZM52.5 92V84H49.5L59.5 75L63.9 78.96C63.04 79.69 62.5 80.78 62.5 82V88C61.87 88.83 61.5 89.87 61.5 91L61.6 92H52.5Z" fill="black"/>
</mask>
<g mask="url(#mask1_4744_39984)">
<rect x="47.5" y="72" width="24" height="24" fill="#03A9F4"/>
</g>
<path d="M82 72C82 67.5817 85.5817 64 90 64H111C115.418 64 119 67.5817 119 72V96C119 100.418 115.418 104 111 104H90C85.5817 104 82 100.418 82 96V72Z" fill="white"/>
<path d="M90 64.5H111C115.142 64.5 118.5 67.8579 118.5 72V96C118.5 100.142 115.142 103.5 111 103.5H90C85.8579 103.5 82.5 100.142 82.5 96V72C82.5 67.8579 85.8579 64.5 90 64.5Z" stroke="black" stroke-opacity="0.12"/>
<mask id="mask2_4744_39984" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="88" y="72" width="25" height="24">
<path d="M100.5 84H107.5C106.97 88.11 104.22 91.78 100.5 92.92V84H93.5V78.3L100.5 75.19M100.5 73L91.5 77V83C91.5 88.55 95.34 93.73 100.5 95C105.66 93.73 109.5 88.55 109.5 83V77L100.5 73Z" fill="black"/>
</mask>
<g mask="url(#mask2_4744_39984)">
<rect x="88.5" y="72" width="24" height="24" fill="#03A9F4"/>
</g>
<path d="M123 72C123 67.5817 126.582 64 131 64H152C156.418 64 160 67.5817 160 72V96C160 100.418 156.418 104 152 104H131C126.582 104 123 100.418 123 96V72Z" fill="white"/>
<path d="M131 64.5H152C156.142 64.5 159.5 67.8579 159.5 72V96C159.5 100.142 156.142 103.5 152 103.5H131C126.858 103.5 123.5 100.142 123.5 96V72C123.5 67.8579 126.858 64.5 131 64.5Z" stroke="black" stroke-opacity="0.12"/>
<mask id="mask3_4744_39984" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="129" y="72" width="25" height="24">
<path d="M145.5 84C145.5 83.4696 145.711 82.9609 146.086 82.5858C146.461 82.2107 146.97 82 147.5 82C148.03 82 148.539 82.2107 148.914 82.5858C149.289 82.9609 149.5 83.4696 149.5 84C149.5 84.5304 149.289 85.0391 148.914 85.4142C148.539 85.7893 148.03 86 147.5 86C146.97 86 146.461 85.7893 146.086 85.4142C145.711 85.0391 145.5 84.5304 145.5 84ZM139.5 84C139.5 83.4696 139.711 82.9609 140.086 82.5858C140.461 82.2107 140.97 82 141.5 82C142.03 82 142.539 82.2107 142.914 82.5858C143.289 82.9609 143.5 83.4696 143.5 84C143.5 84.5304 143.289 85.0391 142.914 85.4142C142.539 85.7893 142.03 86 141.5 86C140.97 86 140.461 85.7893 140.086 85.4142C139.711 85.0391 139.5 84.5304 139.5 84ZM133.5 84C133.5 83.4696 133.711 82.9609 134.086 82.5858C134.461 82.2107 134.97 82 135.5 82C136.03 82 136.539 82.2107 136.914 82.5858C137.289 82.9609 137.5 83.4696 137.5 84C137.5 84.5304 137.289 85.0391 136.914 85.4142C136.539 85.7893 136.03 86 135.5 86C134.97 86 134.461 85.7893 134.086 85.4142C133.711 85.0391 133.5 84.5304 133.5 84Z" fill="black"/>
</mask>
<g mask="url(#mask3_4744_39984)">
<rect x="129.5" y="72" width="24" height="24" fill="#18BCF2"/>
</g>
<path d="M0 116C0 113.791 1.79086 112 4 112H29C31.2091 112 33 113.791 33 116C33 118.209 31.2091 120 29 120H4C1.79086 120 0 118.209 0 116Z" fill="black" fill-opacity="0.32"/>
<path d="M0 132C0 127.582 3.58172 124 8 124H70C74.4183 124 78 127.582 78 132V160H0V132Z" fill="url(#paint0_linear_4744_39984)"/>
<path d="M8 124.5H70C74.1421 124.5 77.5 127.858 77.5 132V159.5H0.5V132C0.5 127.858 3.85786 124.5 8 124.5Z" stroke="url(#paint1_linear_4744_39984)" stroke-opacity="0.12"/>
<path d="M82 132C82 127.582 85.5817 124 90 124H152C156.418 124 160 127.582 160 132V160H82V132Z" fill="url(#paint2_linear_4744_39984)"/>
<path d="M90 124.5H152C156.142 124.5 159.5 127.858 159.5 132V159.5H82.5V132C82.5 127.858 85.8579 124.5 90 124.5Z" stroke="url(#paint3_linear_4744_39984)" stroke-opacity="0.12"/>
</g>
<defs>
<linearGradient id="paint0_linear_4744_39984" x1="39" y1="124" x2="39" y2="160" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_4744_39984" x1="39" y1="124" x2="39" y2="160" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-opacity="0.12"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint2_linear_4744_39984" x1="121" y1="124" x2="121" y2="160" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint3_linear_4744_39984" x1="121" y1="124" x2="121" y2="160" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-opacity="0.12"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<clipPath id="clip0_4744_39984">
<rect width="160" height="160" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@@ -1,16 +1,64 @@
import memoizeOne from "memoize-one";
import { theme2hex } from "./convert-color";
// Total number of colors defined in CSS variables (--color-1 through --color-54)
export const COLORS_COUNT = 54;
export const COLORS = [
"#4269d0",
"#f4bd4a",
"#ff725c",
"#6cc5b0",
"#a463f2",
"#ff8ab7",
"#9c6b4e",
"#97bbf5",
"#01ab63",
"#094bad",
"#c99000",
"#d84f3e",
"#49a28f",
"#048732",
"#d96895",
"#8043ce",
"#7599d1",
"#7a4c31",
"#6989f4",
"#ffd444",
"#ff957c",
"#8fe9d3",
"#62cc71",
"#ffadda",
"#c884ff",
"#badeff",
"#bf8b6d",
"#927acc",
"#97ee3f",
"#bf3947",
"#9f5b00",
"#f48758",
"#8caed6",
"#f2b94f",
"#eff26e",
"#e43872",
"#d9b100",
"#9d7a00",
"#698cff",
"#00d27e",
"#d06800",
"#009f82",
"#c49200",
"#cbe8ff",
"#fecddf",
"#c27eb6",
"#8cd2ce",
"#c4b8d9",
"#f883b0",
"#a49100",
"#f48800",
"#27d0df",
"#a04a9b",
];
export function getColorByIndex(
index: number,
style: CSSStyleDeclaration
): string {
// Wrap around using modulo to support unlimited indices
const colorIndex = (index % COLORS_COUNT) + 1;
return style.getPropertyValue(`--color-${colorIndex}`);
export function getColorByIndex(index: number) {
return COLORS[index % COLORS.length];
}
export function getGraphColorByIndex(
@@ -20,19 +68,15 @@ export function getGraphColorByIndex(
// The CSS vars for the colors use range 1..n, so we need to adjust the index from the internal 0..n color index range.
const themeColor =
style.getPropertyValue(`--graph-color-${index + 1}`) ||
getColorByIndex(index, style);
getColorByIndex(index);
return theme2hex(themeColor);
}
export const getAllGraphColors = memoizeOne(
(style: CSSStyleDeclaration) =>
Array.from({ length: COLORS_COUNT }, (_, index) =>
getGraphColorByIndex(index, style)
),
COLORS.map((_color, index) => getGraphColorByIndex(index, style)),
(newArgs: [CSSStyleDeclaration], lastArgs: [CSSStyleDeclaration]) =>
// this is not ideal, but we need to memoize the colors
newArgs[0].getPropertyValue("--graph-color-1") ===
lastArgs[0].getPropertyValue("--graph-color-1") &&
newArgs[0].getPropertyValue("--color-1") ===
lastArgs[0].getPropertyValue("--color-1")
lastArgs[0].getPropertyValue("--graph-color-1")
);

View File

@@ -651,18 +651,6 @@ export class HaCodeEditor extends ReactiveElement {
}
}
}
// Properties that should never suggest entities
const negativeProperties = ["action"];
// Create regex pattern for negative properties
const negativePropertyPattern = negativeProperties.join("|");
const negativeEntityFieldRegex = new RegExp(
`^\\s*(-\\s+)?(${negativePropertyPattern}):\\s*`
);
if (lineText.match(negativeEntityFieldRegex)) {
return null;
}
}
// Original entity completion logic for non-YAML or when not in entity_id field

View File

@@ -1,84 +0,0 @@
import {
mdiAmpersand,
mdiClockOutline,
mdiCodeBraces,
mdiDevices,
mdiGateOr,
mdiIdentifier,
mdiMapMarkerRadius,
mdiNotEqualVariant,
mdiNumeric,
mdiStateMachine,
mdiWeatherSunny,
} from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { computeDomain } from "../common/entity/compute_domain";
import { conditionIcon, FALLBACK_DOMAIN_ICONS } from "../data/icons";
import type { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
export const CONDITION_ICONS = {
device: mdiDevices,
and: mdiAmpersand,
or: mdiGateOr,
not: mdiNotEqualVariant,
state: mdiStateMachine,
numeric_state: mdiNumeric,
sun: mdiWeatherSunny,
template: mdiCodeBraces,
time: mdiClockOutline,
trigger: mdiIdentifier,
zone: mdiMapMarkerRadius,
};
@customElement("ha-condition-icon")
export class HaConditionIcon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public condition?: string;
@property() public icon?: string;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
}
if (!this.condition) {
return nothing;
}
if (!this.hass) {
return this._renderFallback();
}
const icon = conditionIcon(this.hass, this.condition).then((icn) => {
if (icn) {
return html`<ha-icon .icon=${icn}></ha-icon>`;
}
return this._renderFallback();
});
return html`${until(icon)}`;
}
private _renderFallback() {
const domain = computeDomain(this.condition!);
return html`
<ha-svg-icon
.path=${CONDITION_ICONS[this.condition!] ||
FALLBACK_DOMAIN_ICONS[domain]}
></ha-svg-icon>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-condition-icon": HaConditionIcon;
}
}

View File

@@ -75,15 +75,11 @@ export class HaDialogHeader extends LitElement {
font-size: var(--ha-font-size-xl);
line-height: var(--ha-line-height-condensed);
font-weight: var(--ha-font-weight-medium);
color: var(--ha-dialog-header-title-color, var(--primary-text-color));
}
.header-subtitle {
font-size: var(--ha-font-size-m);
line-height: var(--ha-line-height-normal);
color: var(
--ha-dialog-header-subtitle-color,
var(--secondary-text-color)
);
color: var(--secondary-text-color);
}
@media all and (min-width: 450px) and (min-height: 500px) {
.header-bar {

View File

@@ -209,7 +209,6 @@ export class HaExpansionPanel extends LitElement {
::slotted([slot="header"]) {
flex: 1;
overflow-wrap: anywhere;
color: var(--primary-text-color);
}
.container {

View File

@@ -66,7 +66,7 @@ export class HaIconOverflowMenu extends LitElement {
.path=${item.path}
></ha-svg-icon>
${item.label}
</ha-md-menu-item>`
</ha-md-menu-item> `
)}
</ha-md-button-menu>`
: html`
@@ -103,7 +103,6 @@ export class HaIconOverflowMenu extends LitElement {
:host {
display: flex;
justify-content: flex-end;
cursor: initial;
}
div[role="separator"] {
border-right: 1px solid var(--divider-color);

View File

@@ -27,7 +27,6 @@ export interface DisplayItem {
label: string;
description?: string;
disableSorting?: boolean;
disableHiding?: boolean;
}
export interface DisplayValue {
@@ -102,7 +101,6 @@ export class HaItemDisplayEditor extends LitElement {
icon,
iconPath,
disableSorting,
disableHiding,
} = item;
return html`
<ha-md-list-item
@@ -157,21 +155,18 @@ export class HaItemDisplayEditor extends LitElement {
</div>
`
: nothing}
${!isVisible || !disableHiding
? html`<ha-icon-button
.path=${isVisible ? mdiEye : mdiEyeOff}
slot="end"
.label=${this.hass.localize(
`ui.components.items-display-editor.${isVisible ? "hide" : "show"}`,
{
label: label,
}
)}
.value=${value}
@click=${this._toggle}
.disabled=${disableHiding || false}
></ha-icon-button>`
: nothing}
<ha-icon-button
.path=${isVisible ? mdiEye : mdiEyeOff}
slot="end"
.label=${this.hass.localize(
`ui.components.items-display-editor.${isVisible ? "hide" : "show"}`,
{
label: label,
}
)}
.value=${value}
@click=${this._toggle}
></ha-icon-button>
${isVisible && !disableSorting
? html`
<ha-svg-icon

View File

@@ -81,7 +81,6 @@ class HaLabel extends LitElement {
.container {
display: flex;
align-items: center;
position: relative;
height: 100%;
padding: 0 16px;

View File

@@ -36,11 +36,6 @@ export class HaMdMenuItem extends MenuItemEl {
::slotted([slot="headline"]) {
text-wrap: nowrap;
}
:host([disabled]) {
opacity: 1;
--md-menu-item-label-text-color: var(--disabled-text-color);
--md-menu-item-leading-icon-color: var(--disabled-text-color);
}
`,
];
}

View File

@@ -6,7 +6,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { titleCase } from "../common/string/title-case";
import { fetchConfig } from "../data/lovelace/config/types";
import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
import { getPanelIcon, getPanelTitle } from "../data/panel";
import { getDefaultPanelUrlPath } from "../data/panel";
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
@@ -43,8 +43,13 @@ const createViewNavigationItem = (
const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
path: `/${panel.url_path}`,
icon: getPanelIcon(panel) || "mdi:view-dashboard",
title: getPanelTitle(hass, panel) || "",
icon: panel.icon ?? "mdi:view-dashboard",
title:
panel.url_path === getDefaultPanelUrlPath(hass)
? hass.localize("panel.states")
: hass.localize(`panel.${panel.title}`) ||
panel.title ||
(panel.url_path ? titleCase(panel.url_path) : ""),
});
@customElement("ha-navigation-picker")

View File

@@ -465,16 +465,10 @@ export class HaServiceControl extends LitElement {
? computeObjectId(this._value.action)
: undefined;
const descriptionPlaceholders =
domain && serviceName
? this.hass.services[domain][serviceName].description_placeholders
: undefined;
const description =
(serviceName &&
this.hass.localize(
`component.${domain}.services.${serviceName}.description`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.description`
)) ||
serviceData?.description;
@@ -543,8 +537,7 @@ export class HaServiceControl extends LitElement {
.disabled=${this.disabled}
.value=${this._value?.data?.entity_id}
.label=${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.entity_id.description`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.fields.entity_id.description`
) || entityId.description}
@value-changed=${this._entityPicked}
allow-custom-entity
@@ -582,8 +575,7 @@ export class HaServiceControl extends LitElement {
left-chevron
.expanded=${!dataField.collapsed}
.header=${this.hass.localize(
`component.${domain}.services.${serviceName}.sections.${dataField.key}.name`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.sections.${dataField.key}.name`
) ||
dataField.name ||
dataField.key}
@@ -619,10 +611,7 @@ export class HaServiceControl extends LitElement {
serviceName: string | undefined
) {
return this.hass!.localize(
`component.${domain}.services.${serviceName}.sections.${dataField.key}.description`,
domain && serviceName
? this.hass.services[domain][serviceName].description_placeholders
: undefined
`component.${domain}.services.${serviceName}.sections.${dataField.key}.description`
);
}
@@ -669,10 +658,6 @@ export class HaServiceControl extends LitElement {
}
const showOptional = showOptionalToggle(dataField);
const descriptionPlaceholders =
domain && serviceName
? this.hass.services[domain][serviceName].description_placeholders
: undefined;
return dataField.selector &&
(!dataField.advanced ||
@@ -694,8 +679,7 @@ export class HaServiceControl extends LitElement {
></ha-checkbox>`}
<span slot="heading"
>${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name`
) ||
dataField.name ||
dataField.key}</span
@@ -705,8 +689,7 @@ export class HaServiceControl extends LitElement {
breaks
allow-svg
.content=${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
) || dataField?.description}
></ha-markdown>
</span>

View File

@@ -92,14 +92,8 @@ class HaServicePicker extends LitElement {
`;
}
const descriptionPlaceholders =
this.hass.services[domain][service].description_placeholders;
const serviceName =
localize(
`component.${domain}.services.${service}.name`,
descriptionPlaceholders
) ||
localize(`component.${domain}.services.${service}.name`) ||
services[domain][service].name ||
service;
@@ -169,21 +163,16 @@ class HaServicePicker extends LitElement {
const serviceId = `${domain}.${service}`;
const domainName = domainToName(localize, domain);
const descriptionPlaceholders =
this.hass.services[domain][service].description_placeholders;
const name =
this.hass.localize(
`component.${domain}.services.${service}.name`,
descriptionPlaceholders
`component.${domain}.services.${service}.name`
) ||
services[domain][service].name ||
service;
const description =
this.hass.localize(
`component.${domain}.services.${service}.description`,
descriptionPlaceholders
`component.${domain}.services.${service}.description`
) ||
services[domain][service].description ||
"";

View File

@@ -1,13 +1,22 @@
import {
mdiBell,
mdiCalendar,
mdiCellphoneCog,
mdiChartBox,
mdiClipboardList,
mdiCog,
mdiFormatListBulletedType,
mdiHammer,
mdiLightningBolt,
mdiMenu,
mdiMenuOpen,
mdiPlayBoxMultiple,
mdiTooltipAccount,
mdiViewDashboard,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { LitElement, css, html, nothing } from "lit";
import {
customElement,
eventOptions,
@@ -24,14 +33,7 @@ import { computeRTL } from "../common/util/compute_rtl";
import { throttle } from "../common/util/throttle";
import { subscribeFrontendUserData } from "../data/frontend";
import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
import {
FIXED_PANELS,
getDefaultPanelUrlPath,
getPanelIcon,
getPanelIconPath,
getPanelTitle,
SHOW_AFTER_SPACER_PANELS,
} from "../data/panel";
import { getDefaultPanelUrlPath } from "../data/panel";
import type { PersistentNotification } from "../data/persistent_notification";
import { subscribeNotifications } from "../data/persistent_notification";
import { subscribeRepairsIssueRegistry } from "../data/repairs";
@@ -52,6 +54,8 @@ import "./ha-spinner";
import "./ha-svg-icon";
import "./user/ha-user-badge";
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
const SORT_VALUE_URL_PATHS = {
@@ -63,6 +67,18 @@ const SORT_VALUE_URL_PATHS = {
config: 11,
};
export const PANEL_ICONS = {
calendar: mdiCalendar,
"developer-tools": mdiHammer,
energy: mdiLightningBolt,
history: mdiChartBox,
logbook: mdiFormatListBulletedType,
lovelace: mdiViewDashboard,
map: mdiTooltipAccount,
"media-browser": mdiPlayBoxMultiple,
todo: mdiClipboardList,
};
const panelSorter = (
reverseSort: string[],
defaultPanel: string,
@@ -139,23 +155,16 @@ export const computePanels = memoizeOne(
const beforeSpacer: PanelInfo[] = [];
const afterSpacer: PanelInfo[] = [];
const allPanels = Object.values(panels).filter(
(panel) => !FIXED_PANELS.includes(panel.url_path)
);
allPanels.forEach((panel) => {
const isDefaultPanel = panel.url_path === defaultPanel;
Object.values(panels).forEach((panel) => {
if (
!isDefaultPanel &&
(!panel.title ||
hiddenPanels.includes(panel.url_path) ||
(panel.default_visible === false &&
!panelsOrder.includes(panel.url_path)))
hiddenPanels.includes(panel.url_path) ||
(!panel.title && panel.url_path !== defaultPanel) ||
(panel.default_visible === false &&
!panelsOrder.includes(panel.url_path))
) {
return;
}
(SHOW_AFTER_SPACER_PANELS.includes(panel.url_path)
(SHOW_AFTER_SPACER.includes(panel.url_path)
? afterSpacer
: beforeSpacer
).push(panel);
@@ -242,7 +251,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return nothing;
}
const selectedPanel = this.hass.panelUrl;
// Show the supervisor as being part of configuration
const selectedPanel = this.route.path?.startsWith("/hassio/")
? "config"
: this.hass.panelUrl;
// prettier-ignore
return html`
@@ -385,9 +397,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _renderAllPanels(selectedPanel: string) {
if (!this._panelOrder || !this._hiddenPanels) {
return html`
<ha-fade-in .delay=${500}>
<ha-spinner size="small"></ha-spinner>
</ha-fade-in>
<ha-fade-in .delay=${500}
><ha-spinner size="small"></ha-spinner
></ha-fade-in>
`;
}
@@ -401,6 +413,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this.hass.locale
);
// prettier-ignore
return html`
<ha-md-list
class="ha-scrollbar"
@@ -409,42 +422,61 @@ class HaSidebar extends SubscribeMixin(LitElement) {
@scroll=${this._listboxScroll}
@keydown=${this._listboxKeydown}
>
${this._renderPanels(beforeSpacer, selectedPanel)}
${this._renderPanels(beforeSpacer, selectedPanel, defaultPanel)}
${this._renderSpacer()}
${this._renderPanels(afterSpacer, selectedPanel)}
${this.hass.user?.is_admin
? this._renderConfiguration(selectedPanel)
: this._renderExternalConfiguration()}
${this._renderPanels(afterSpacer, selectedPanel, defaultPanel)}
${this._renderExternalConfiguration()}
</ha-md-list>
`;
}
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
private _renderPanels(
panels: PanelInfo[],
selectedPanel: string,
defaultPanel: string
) {
return panels.map((panel) =>
this._renderPanel(panel, panel.url_path === selectedPanel)
this._renderPanel(
panel.url_path,
panel.url_path === defaultPanel
? panel.title || this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) || panel.title,
panel.icon,
panel.url_path === defaultPanel && !panel.icon
? PANEL_ICONS.lovelace
: panel.url_path in PANEL_ICONS
? PANEL_ICONS[panel.url_path]
: undefined,
selectedPanel
)
);
}
private _renderPanel(panel: PanelInfo, isSelected: boolean) {
const title = getPanelTitle(this.hass, panel);
const urlPath = panel.url_path;
const icon = getPanelIcon(panel);
const iconPath = getPanelIconPath(panel);
return html`
<ha-md-list-item
.href=${`/${urlPath}`}
type="link"
class=${classMap({ selected: isSelected })}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
${iconPath
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
<span class="item-text" slot="headline">${title}</span>
</ha-md-list-item>
`;
private _renderPanel(
urlPath: string,
title: string | null,
icon: string | null | undefined,
iconPath: string | null | undefined,
selectedPanel: string
) {
return urlPath === "config"
? this._renderConfiguration(title, selectedPanel)
: html`
<ha-md-list-item
.href=${`/${urlPath}`}
type="link"
class=${classMap({
selected: selectedPanel === urlPath,
})}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
${iconPath
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
<span class="item-text" slot="headline">${title}</span>
</ha-md-list-item>
`;
}
private _renderDivider() {
@@ -455,15 +487,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return html`<div class="spacer" disabled></div>`;
}
private _renderConfiguration(selectedPanel: string) {
if (!this.hass.user?.is_admin) {
return nothing;
}
const isSelected =
selectedPanel === "config" || this.route.path?.startsWith("/hassio/");
private _renderConfiguration(title: string | null, selectedPanel: string) {
return html`
<ha-md-list-item
class="configuration ${classMap({ selected: isSelected })}"
class="configuration${selectedPanel === "config" ? " selected" : ""}"
type="button"
href="/config"
@mouseenter=${this._itemMouseEnter}
@@ -477,17 +504,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
${this._updatesCount + this._issuesCount}
</span>
`
: nothing}
<span class="item-text" slot="headline"
>${this.hass.localize("panel.config")}</span
>
: ""}
<span class="item-text" slot="headline">${title}</span>
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
? html`
<span class="badge" slot="end"
>${this._updatesCount + this._issuesCount}</span
>
`
: nothing}
: ""}
</ha-md-list-item>
`;
}
@@ -510,20 +535,19 @@ class HaSidebar extends SubscribeMixin(LitElement) {
? html`
<span class="badge" slot="start"> ${notificationCount} </span>
`
: nothing}
: ""}
<span class="item-text" slot="headline"
>${this.hass.localize("ui.notification_drawer.title")}</span
>
${this.alwaysExpand && notificationCount > 0
? html`<span class="badge" slot="end">${notificationCount}</span>`
: nothing}
: ""}
</ha-md-list-item>
`;
}
private _renderUserItem(selectedPanel: string) {
const isRTL = computeRTL(this.hass);
const isSelected = selectedPanel === "profile";
return html`
<ha-md-list-item
@@ -531,7 +555,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
type="link"
class=${classMap({
user: true,
selected: isSelected,
selected: selectedPanel === "profile",
rtl: isRTL,
})}
@mouseenter=${this._itemMouseEnter}
@@ -542,30 +566,31 @@ class HaSidebar extends SubscribeMixin(LitElement) {
.user=${this.hass.user}
.hass=${this.hass}
></ha-user-badge>
<span class="item-text" slot="headline">
${this.hass.user ? this.hass.user.name : ""}
</span>
<span class="item-text" slot="headline"
>${this.hass.user ? this.hass.user.name : ""}</span
>
</ha-md-list-item>
`;
}
private _renderExternalConfiguration() {
if (!this.hass.auth.external?.config.hasSettingsScreen) {
return nothing;
}
return html`
<ha-md-list-item
@click=${this._handleExternalAppConfiguration}
type="button"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
<span class="item-text" slot="headline">
${this.hass.localize("ui.sidebar.external_app_configuration")}
</span>
</ha-md-list-item>
`;
return html`${!this.hass.user?.is_admin &&
this.hass.auth.external?.config.hasSettingsScreen
? html`
<ha-md-list-item
@click=${this._handleExternalAppConfiguration}
type="button"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
<span class="item-text" slot="headline">
${this.hass.localize("ui.sidebar.external_app_configuration")}
</span>
</ha-md-list-item>
`
: ""}`;
}
private _handleExternalAppConfiguration(ev: Event) {

View File

@@ -1,178 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { HomeAssistant } from "../types";
import { subscribeLabFeatures } from "../data/labs";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
interface Snowflake {
id: number;
left: number;
size: number;
duration: number;
delay: number;
blur: number;
}
@customElement("ha-snowflakes")
export class HaSnowflakes extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@state() private _enabled = false;
@state() private _snowflakes: Snowflake[] = [];
private _maxSnowflakes = 50;
public hassSubscribe() {
return [
subscribeLabFeatures(this.hass!.connection, (features) => {
this._enabled =
features.find(
(f) =>
f.domain === "frontend" && f.preview_feature === "winter_mode"
)?.enabled ?? false;
}),
];
}
private _generateSnowflakes() {
if (!this._enabled) {
this._snowflakes = [];
return;
}
const snowflakes: Snowflake[] = [];
for (let i = 0; i < this._maxSnowflakes; i++) {
snowflakes.push({
id: i,
left: Math.random() * 100, // Random position from 0-100%
size: Math.random() * 12 + 8, // Random size between 8-20px
duration: Math.random() * 8 + 8, // Random duration between 8-16s
delay: Math.random() * 8, // Random delay between 0-8s
blur: Math.random() * 1, // Random blur between 0-1px
});
}
this._snowflakes = snowflakes;
}
protected willUpdate(changedProps: Map<string, unknown>) {
super.willUpdate(changedProps);
if (changedProps.has("_enabled")) {
this._generateSnowflakes();
}
}
protected render() {
if (!this._enabled) {
return nothing;
}
const isDark = this.hass?.themes.darkMode ?? false;
return html`
<div class="snowflakes ${isDark ? "dark" : "light"}" aria-hidden="true">
${this._snowflakes.map(
(flake) => html`
<div
class="snowflake ${this.narrow && flake.id >= 30
? "hide-narrow"
: ""}"
style="
left: ${flake.left}%;
font-size: ${flake.size}px;
animation-duration: ${flake.duration}s;
animation-delay: ${flake.delay}s;
filter: blur(${flake.blur}px);
"
>
</div>
`
)}
</div>
`;
}
static readonly styles = css`
:host {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 9999;
overflow: hidden;
}
.snowflakes {
position: absolute;
top: -10%;
left: 0;
width: 100%;
height: 110%;
pointer-events: none;
}
.snowflake {
position: absolute;
top: -10%;
opacity: 0.7;
user-select: none;
pointer-events: none;
animation: fall linear infinite;
}
.light .snowflake {
color: #00bcd4;
text-shadow:
0 0 5px #00bcd4,
0 0 10px #00e5ff;
}
.dark .snowflake {
color: #fff;
text-shadow:
0 0 5px rgba(255, 255, 255, 0.8),
0 0 10px rgba(255, 255, 255, 0.5);
}
.snowflake.hide-narrow {
display: none;
}
@keyframes fall {
0% {
transform: translateY(-10vh) translateX(0);
}
25% {
transform: translateY(30vh) translateX(10px);
}
50% {
transform: translateY(60vh) translateX(-10px);
}
75% {
transform: translateY(85vh) translateX(10px);
}
100% {
transform: translateY(120vh) translateX(0);
}
}
@media (prefers-reduced-motion: reduce) {
.snowflake {
animation: none;
display: none;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-snowflakes": HaSnowflakes;
}
}

View File

@@ -5,7 +5,6 @@ export interface AnalyticsPreferences {
diagnostics?: boolean;
usage?: boolean;
statistics?: boolean;
snapshots?: boolean;
}
export interface Analytics {

View File

@@ -214,8 +214,6 @@ export interface PipelineRun {
stage: "ready" | "wake_word" | "stt" | "intent" | "tts" | "done" | "error";
run: PipelineRunStartEvent["data"];
error?: PipelineErrorEvent["data"];
started: Date;
finished?: Date;
wake_word?: PipelineWakeWordStartEvent["data"] &
Partial<PipelineWakeWordEndEvent["data"]> & { done: boolean };
stt?: PipelineSTTStartEvent["data"] &
@@ -237,7 +235,6 @@ export const processEvent = (
stage: "ready",
run: event.data,
events: [event],
started: new Date(event.timestamp),
};
return run;
}
@@ -293,14 +290,9 @@ export const processEvent = (
tts: { ...run.tts!, ...event.data, done: true },
};
} else if (event.type === "run-end") {
run = { ...run, finished: new Date(event.timestamp), stage: "done" };
run = { ...run, stage: "done" };
} else if (event.type === "error") {
run = {
...run,
finished: new Date(event.timestamp),
stage: "error",
error: event.data,
};
run = { ...run, stage: "error", error: event.data };
} else {
run = { ...run };
}

View File

@@ -10,7 +10,6 @@ import type { LocalizeKeys } from "../common/translations/localize";
import { createSearchParam } from "../common/url/search-params";
import type { Context, HomeAssistant } from "../types";
import type { BlueprintInput } from "./blueprint";
import type { ConditionDescription } from "./condition";
import { CONDITION_BUILDING_BLOCKS } from "./condition";
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
import type { Action, Field, MODES } from "./script";
@@ -237,12 +236,6 @@ interface BaseCondition {
condition: string;
alias?: string;
enabled?: boolean;
options?: Record<string, unknown>;
}
export interface PlatformCondition extends BaseCondition {
condition: Exclude<string, LegacyCondition["condition"]>;
target?: HassServiceTarget;
}
export interface LogicalCondition extends BaseCondition {
@@ -327,7 +320,7 @@ export type AutomationElementGroup = Record<
{ icon?: string; members?: AutomationElementGroup }
>;
export type LegacyCondition =
export type Condition =
| StateCondition
| NumericStateCondition
| SunCondition
@@ -338,8 +331,6 @@ export type LegacyCondition =
| LogicalCondition
| TriggerCondition;
export type Condition = LegacyCondition | PlatformCondition;
export type ConditionWithShorthand =
| Condition
| ShorthandAndConditionList
@@ -617,7 +608,6 @@ export interface ConditionSidebarConfig extends BaseSidebarConfig {
insertAfter: (value: Condition | Condition[]) => boolean;
toggleYamlMode: () => void;
config: Condition;
description?: ConditionDescription;
yamlMode: boolean;
uiSupported: boolean;
}

View File

@@ -18,14 +18,7 @@ import {
} from "../common/string/format-list";
import { hasTemplate } from "../common/string/has-template";
import type { HomeAssistant } from "../types";
import type {
Condition,
ForDict,
LegacyCondition,
LegacyTrigger,
Trigger,
} from "./automation";
import { getConditionDomain, getConditionObjectId } from "./condition";
import type { Condition, ForDict, LegacyTrigger, Trigger } from "./automation";
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
import {
localizeDeviceAutomationCondition,
@@ -903,39 +896,6 @@ const tryDescribeCondition = (
}
}
const description = describeLegacyCondition(
condition as LegacyCondition,
hass,
entityRegistry
);
if (description) {
return description;
}
const conditionType = condition.condition;
const domain = getConditionDomain(condition.condition);
const type = getConditionObjectId(condition.condition);
return (
hass.localize(
`component.${domain}.conditions.${type}.description_configured`
) ||
hass.localize(
`ui.panel.config.automation.editor.conditions.type.${conditionType as LegacyCondition["condition"]}.label`
) ||
hass.localize(
`ui.panel.config.automation.editor.conditions.unknown_condition`
)
);
};
const describeLegacyCondition = (
condition: LegacyCondition,
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[]
) => {
if (condition.condition === "or") {
const conditions = ensureArray(condition.conditions);
@@ -1327,5 +1287,12 @@ const describeLegacyCondition = (
);
}
return undefined;
return (
hass.localize(
`ui.panel.config.automation.editor.conditions.type.${condition.condition}.label`
) ||
hass.localize(
`ui.panel.config.automation.editor.conditions.unknown_condition`
)
);
};

View File

@@ -137,12 +137,8 @@ const getCalendarDate = (dateObj: any): string | undefined => {
return undefined;
};
export const getCalendars = (
hass: HomeAssistant,
element: Element
): Calendar[] => {
const computedStyles = getComputedStyle(element);
return Object.keys(hass.states)
export const getCalendars = (hass: HomeAssistant): Calendar[] =>
Object.keys(hass.states)
.filter(
(eid) =>
computeDomain(eid) === "calendar" &&
@@ -153,9 +149,8 @@ export const getCalendars = (
.map((eid, idx) => ({
...hass.states[eid],
name: computeStateName(hass.states[eid]),
backgroundColor: getColorByIndex(idx, computedStyles),
backgroundColor: getColorByIndex(idx),
}));
};
export const createCalendarEvent = (
hass: HomeAssistant,

View File

@@ -1,228 +0,0 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
export const enum ChatLogEventType {
INITIAL_STATE = "initial_state",
CREATED = "created",
UPDATED = "updated",
DELETED = "deleted",
CONTENT_ADDED = "content_added",
}
export interface ChatLogAttachment {
media_content_id: string;
mime_type: string;
path: string;
}
export interface ChatLogSystemContent {
role: "system";
content: string;
created: Date;
}
export interface ChatLogUserContent {
role: "user";
content: string;
created: Date;
attachments?: ChatLogAttachment[];
}
export interface ChatLogAssistantContent {
role: "assistant";
agent_id: string;
created: Date;
content?: string;
thinking_content?: string;
tool_calls?: any[];
}
export interface ChatLogToolResultContent {
role: "tool_result";
agent_id: string;
tool_call_id: string;
tool_name: string;
tool_result: any;
created: Date;
}
export type ChatLogContent =
| ChatLogSystemContent
| ChatLogUserContent
| ChatLogAssistantContent
| ChatLogToolResultContent;
export interface ChatLog {
conversation_id: string;
continue_conversation: boolean;
content: ChatLogContent[];
created: Date;
}
// Internal wire format types (not exported)
interface ChatLogSystemContentWire {
role: "system";
content: string;
created: string;
}
interface ChatLogUserContentWire {
role: "user";
content: string;
created: string;
attachments?: ChatLogAttachment[];
}
interface ChatLogAssistantContentWire {
role: "assistant";
agent_id: string;
created: string;
content?: string;
thinking_content?: string;
tool_calls?: {
tool_name: string;
tool_args: Record<string, any>;
id: string;
external: boolean;
}[];
}
interface ChatLogToolResultContentWire {
role: "tool_result";
agent_id: string;
tool_call_id: string;
tool_name: string;
tool_result: any;
created: string;
}
type ChatLogContentWire =
| ChatLogSystemContentWire
| ChatLogUserContentWire
| ChatLogAssistantContentWire
| ChatLogToolResultContentWire;
interface ChatLogWire {
conversation_id: string;
continue_conversation: boolean;
content: ChatLogContentWire[];
created: string;
}
const processContent = (content: ChatLogContentWire): ChatLogContent => ({
...content,
created: new Date(content.created),
});
const processChatLog = (chatLog: ChatLogWire): ChatLog => ({
...chatLog,
created: new Date(chatLog.created),
content: chatLog.content.map(processContent),
});
interface ChatLogInitialStateEvent {
event_type: ChatLogEventType.INITIAL_STATE;
data: ChatLogWire;
}
interface ChatLogIndexInitialStateEvent {
event_type: ChatLogEventType.INITIAL_STATE;
data: ChatLogWire[];
}
interface ChatLogCreatedEvent {
conversation_id: string;
event_type: ChatLogEventType.CREATED;
data: ChatLogWire;
}
interface ChatLogUpdatedEvent {
conversation_id: string;
event_type: ChatLogEventType.UPDATED;
data: { chat_log: ChatLogWire };
}
interface ChatLogDeletedEvent {
conversation_id: string;
event_type: ChatLogEventType.DELETED;
data: ChatLogWire;
}
interface ChatLogContentAddedEvent {
conversation_id: string;
event_type: ChatLogEventType.CONTENT_ADDED;
data: { content: ChatLogContentWire };
}
type ChatLogSubscriptionEvent =
| ChatLogInitialStateEvent
| ChatLogUpdatedEvent
| ChatLogDeletedEvent
| ChatLogContentAddedEvent;
type ChatLogIndexSubscriptionEvent =
| ChatLogIndexInitialStateEvent
| ChatLogCreatedEvent
| ChatLogDeletedEvent;
export const subscribeChatLog = (
hass: HomeAssistant,
conversationId: string,
callback: (chatLog: ChatLog | null) => void
): Promise<UnsubscribeFunc> => {
let chatLog: ChatLog | null = null;
return hass.connection.subscribeMessage<ChatLogSubscriptionEvent>(
(event) => {
if (event.event_type === ChatLogEventType.INITIAL_STATE) {
chatLog = processChatLog(event.data);
callback(chatLog);
} else if (event.event_type === ChatLogEventType.CONTENT_ADDED) {
if (chatLog) {
chatLog = {
...chatLog,
content: [...chatLog.content, processContent(event.data.content)],
};
callback(chatLog);
}
} else if (event.event_type === ChatLogEventType.UPDATED) {
chatLog = processChatLog(event.data.chat_log);
callback(chatLog);
} else if (event.event_type === ChatLogEventType.DELETED) {
chatLog = null;
callback(null);
}
},
{
type: "conversation/chat_log/subscribe",
conversation_id: conversationId,
}
);
};
export const subscribeChatLogIndex = (
hass: HomeAssistant,
callback: (chatLogs: ChatLog[]) => void
): Promise<UnsubscribeFunc> => {
let chatLogs: ChatLog[] = [];
return hass.connection.subscribeMessage<ChatLogIndexSubscriptionEvent>(
(event) => {
if (event.event_type === ChatLogEventType.INITIAL_STATE) {
chatLogs = event.data.map(processChatLog);
callback(chatLogs);
} else if (event.event_type === ChatLogEventType.CREATED) {
chatLogs = [...chatLogs, processChatLog(event.data)];
callback(chatLogs);
} else if (event.event_type === ChatLogEventType.DELETED) {
chatLogs = chatLogs.filter(
(chatLog) => chatLog.conversation_id !== event.conversation_id
);
callback(chatLogs);
}
},
{
type: "conversation/chat_log/subscribe_index",
}
);
};

View File

@@ -1,15 +1,38 @@
import { mdiMapClock, mdiShape } from "@mdi/js";
import { computeDomain } from "../common/entity/compute_domain";
import { computeObjectId } from "../common/entity/compute_object_id";
import type { HomeAssistant } from "../types";
import {
mdiAmpersand,
mdiClockOutline,
mdiCodeBraces,
mdiDevices,
mdiGateOr,
mdiIdentifier,
mdiMapClock,
mdiMapMarkerRadius,
mdiNotEqualVariant,
mdiNumeric,
mdiShape,
mdiStateMachine,
mdiWeatherSunny,
} from "@mdi/js";
import type { AutomationElementGroupCollection } from "./automation";
import type { Selector, TargetSelector } from "./selector";
export const CONDITION_ICONS = {
device: mdiDevices,
and: mdiAmpersand,
or: mdiGateOr,
not: mdiNotEqualVariant,
state: mdiStateMachine,
numeric_state: mdiNumeric,
sun: mdiWeatherSunny,
template: mdiCodeBraces,
time: mdiClockOutline,
trigger: mdiIdentifier,
zone: mdiMapMarkerRadius,
};
export const CONDITION_COLLECTIONS: AutomationElementGroupCollection[] = [
{
groups: {
device: {},
dynamicGroups: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
@@ -39,33 +62,3 @@ export const COLLAPSIBLE_CONDITION_ELEMENTS = [
"ha-automation-condition-not",
"ha-automation-condition-or",
];
export interface ConditionDescription {
target?: TargetSelector["target"];
fields: Record<
string,
{
example?: string | boolean | number;
default?: unknown;
required?: boolean;
selector?: Selector;
context?: Record<string, string>;
}
>;
}
export type ConditionDescriptions = Record<string, ConditionDescription>;
export const subscribeConditions = (
hass: HomeAssistant,
callback: (conditions: ConditionDescriptions) => void
) =>
hass.connection.subscribeMessage<ConditionDescriptions>(callback, {
type: "condition_platforms/subscribe",
});
export const getConditionDomain = (condition: string) =>
condition.includes(".") ? computeDomain(condition) : condition;
export const getConditionObjectId = (condition: string) =>
condition.includes(".") ? computeObjectId(condition) : "_";

View File

@@ -200,7 +200,6 @@ export type EnergySource =
export interface EnergyPreferences {
energy_sources: EnergySource[];
device_consumption: DeviceConsumptionEnergyPreference[];
device_consumption_water: DeviceConsumptionEnergyPreference[];
}
export interface EnergyInfo {
@@ -217,7 +216,6 @@ export interface EnergyValidationIssue {
export interface EnergyPreferencesValidation {
energy_sources: EnergyValidationIssue[][];
device_consumption: EnergyValidationIssue[][];
device_consumption_water: EnergyValidationIssue[][];
}
export const getEnergyInfo = (hass: HomeAssistant) =>
@@ -358,11 +356,6 @@ export const getReferencedStatisticIds = (
if (!(includeTypes && !includeTypes.includes("device"))) {
statIDs.push(...prefs.device_consumption.map((d) => d.stat_consumption));
}
if (!(includeTypes && !includeTypes.includes("water"))) {
statIDs.push(
...prefs.device_consumption_water.map((d) => d.stat_consumption)
);
}
return statIDs;
};
@@ -782,7 +775,6 @@ export const getEnergyDataCollection = (
hass.locale,
hass.config
);
collection.refresh();
scheduleUpdatePeriod();
},
addHours(

View File

@@ -7,8 +7,8 @@ export interface CoreFrontendUserData {
}
export interface SidebarFrontendUserData {
panelOrder?: string[];
hiddenPanels?: string[];
panelOrder: string[];
hiddenPanels: string[];
}
export interface CoreFrontendSystemData {

View File

@@ -60,7 +60,6 @@ import type {
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import { getTriggerDomain, getTriggerObjectId } from "./trigger";
import { getConditionDomain, getConditionObjectId } from "./condition";
/** Icon to use when no icon specified for service. */
export const DEFAULT_SERVICE_ICON = mdiRoomService;
@@ -139,25 +138,15 @@ const resources: {
all?: Promise<Record<string, TriggerIcons>>;
domains: Record<string, TriggerIcons | Promise<TriggerIcons>>;
};
conditions: {
all?: Promise<Record<string, ConditionIcons>>;
domains: Record<string, ConditionIcons | Promise<ConditionIcons>>;
};
} = {
entity: {},
entity_component: {},
services: { domains: {} },
triggers: { domains: {} },
conditions: { domains: {} },
};
interface IconResources<
T extends
| ComponentIcons
| PlatformIcons
| ServiceIcons
| TriggerIcons
| ConditionIcons,
T extends ComponentIcons | PlatformIcons | ServiceIcons | TriggerIcons,
> {
resources: Record<string, T>;
}
@@ -206,24 +195,17 @@ type TriggerIcons = Record<
{ trigger: string; sections?: Record<string, string> }
>;
type ConditionIcons = Record<
string,
{ condition: string; sections?: Record<string, string> }
>;
export type IconCategory =
| "entity"
| "entity_component"
| "services"
| "triggers"
| "conditions";
| "triggers";
interface CategoryType {
entity: PlatformIcons;
entity_component: ComponentIcons;
services: ServiceIcons;
triggers: TriggerIcons;
conditions: ConditionIcons;
}
export const getHassIcons = async <T extends IconCategory>(
@@ -345,13 +327,6 @@ export const getTriggerIcons = async (
): Promise<TriggerIcons | Record<string, TriggerIcons> | undefined> =>
getCategoryIcons(hass, "triggers", domain, force);
export const getConditionIcons = async (
hass: HomeAssistant,
domain?: string,
force = false
): Promise<ConditionIcons | Record<string, ConditionIcons> | undefined> =>
getCategoryIcons(hass, "conditions", domain, force);
// Cache for sorted range keys
const sortedRangeCache = new WeakMap<Record<string, string>, number[]>();
@@ -551,25 +526,6 @@ export const triggerIcon = async (
return icon;
};
export const conditionIcon = async (
hass: HomeAssistant,
condition: string
): Promise<string | undefined> => {
let icon: string | undefined;
const domain = getConditionDomain(condition);
const conditionIcons = await getConditionIcons(hass, domain);
if (conditionIcons) {
const conditionName = getConditionObjectId(condition);
const condIcon = conditionIcons[conditionName] as ConditionIcons[string];
icon = condIcon?.condition;
}
if (!icon) {
icon = await domainIcon(hass, domain);
}
return icon;
};
export const serviceIcon = async (
hass: HomeAssistant,
service: string

View File

@@ -1,10 +1,7 @@
import type { MediaSelectorValue } from "../../selector";
import type { LovelaceBadgeConfig } from "./badge";
import type { LovelaceCardConfig } from "./card";
import type {
LovelaceSectionConfig,
LovelaceSectionRawConfig,
} from "./section";
import type { LovelaceSectionRawConfig } from "./section";
import type { LovelaceStrategyConfig } from "./strategy";
export interface ShowViewConfig {
@@ -36,12 +33,6 @@ export interface LovelaceViewHeaderConfig {
badges_wrap?: "wrap" | "scroll";
}
export interface LovelaceViewSidebarConfig {
sections?: LovelaceSectionConfig[];
content_label?: string;
sidebar_label?: string;
}
export interface LovelaceBaseViewConfig {
index?: number;
title?: string;
@@ -65,8 +56,6 @@ export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
cards?: LovelaceCardConfig[];
sections?: LovelaceSectionRawConfig[];
header?: LovelaceViewHeaderConfig;
// Only used for section view, it should move to a section view config type when the views will have dedicated editor.
sidebar?: LovelaceViewSidebarConfig;
}
export interface LovelaceStrategyViewConfig extends LovelaceBaseViewConfig {

View File

@@ -1,15 +1,3 @@
import {
mdiAccount,
mdiCalendar,
mdiChartBox,
mdiClipboardList,
mdiFormatListBulletedType,
mdiHammer,
mdiLightningBolt,
mdiPlayBoxMultiple,
mdiTooltipAccount,
mdiViewDashboard,
} from "@mdi/js";
import type { HomeAssistant, PanelInfo } from "../types";
/** Panel to show when no panel is picked. */
@@ -72,7 +60,7 @@ export const getPanelTitleFromUrlPath = (
return getPanelTitle(hass, panel);
};
export const getPanelIcon = (panel: PanelInfo): string | undefined => {
export const getPanelIcon = (panel: PanelInfo): string | null => {
if (!panel.icon) {
switch (panel.component_name) {
case "profile":
@@ -82,24 +70,5 @@ export const getPanelIcon = (panel: PanelInfo): string | undefined => {
}
}
return panel.icon || undefined;
return panel.icon;
};
export const PANEL_ICON_PATHS = {
calendar: mdiCalendar,
"developer-tools": mdiHammer,
energy: mdiLightningBolt,
history: mdiChartBox,
logbook: mdiFormatListBulletedType,
lovelace: mdiViewDashboard,
profile: mdiAccount,
map: mdiTooltipAccount,
"media-browser": mdiPlayBoxMultiple,
todo: mdiClipboardList,
};
export const getPanelIconPath = (panel: PanelInfo): string | undefined =>
PANEL_ICON_PATHS[panel.url_path];
export const FIXED_PANELS = ["profile", "config"];
export const SHOW_AFTER_SPACER_PANELS = ["developer-tools"];

View File

@@ -219,13 +219,9 @@ const tryDescribeAction = <T extends ActionType>(
if (config.action) {
const [domain, serviceName] = config.action.split(".", 2);
const descriptionPlaceholders =
hass.services[domain][serviceName].description_placeholders;
const service =
hass.localize(
`component.${domain}.services.${serviceName}.name`,
descriptionPlaceholders
) || hass.services[domain][serviceName]?.name;
hass.localize(`component.${domain}.services.${serviceName}.name`) ||
hass.services[domain][serviceName]?.name;
if (config.metadata) {
return hass.localize(

View File

@@ -75,8 +75,7 @@ export type TranslationCategory =
| "preview_features"
| "selector"
| "services"
| "triggers"
| "conditions";
| "triggers";
export const subscribeTranslationPreferences = (
hass: HomeAssistant,

View File

@@ -1,15 +1,15 @@
import { mdiAlertOutline, mdiClose } from "@mdi/js";
import { mdiAlertOutline } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import "../../components/ha-dialog-footer";
import "../../components/ha-dialog-header";
import "../../components/ha-md-dialog";
import type { HaMdDialog } from "../../components/ha-md-dialog";
import "../../components/ha-svg-icon";
import "../../components/ha-textfield";
import type { HaTextField } from "../../components/ha-textfield";
import "../../components/ha-wa-dialog";
import type { HomeAssistant } from "../../types";
import type { DialogBoxParams } from "./show-dialog-box";
@@ -19,12 +19,12 @@ class DialogBox extends LitElement {
@state() private _params?: DialogBoxParams;
@state() private _open = false;
@state() private _closeState?: "canceled" | "confirmed";
@query("ha-textfield") private _textField?: HaTextField;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
private _closePromise?: Promise<void>;
private _closeResolve?: () => void;
@@ -34,7 +34,6 @@ class DialogBox extends LitElement {
await this._closePromise;
}
this._params = params;
this._open = true;
}
public closeDialog(): boolean {
@@ -61,25 +60,16 @@ class DialogBox extends LitElement {
this.hass.localize("ui.dialogs.generic.default_confirmation_title"));
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
?prevent-scrim-close=${confirmPrompt}
<ha-md-dialog
open
.disableCancelAction=${confirmPrompt}
@closed=${this._dialogClosed}
type="alert"
aria-labelledby="dialog-box-title"
aria-describedby="dialog-box-description"
>
<ha-dialog-header slot="header">
${!confirmPrompt
? html`<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-dialog="close"
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button
></slot>`
: nothing}
<span slot="title" id="dialog-box-title">
<div slot="headline">
<span .title=${dialogTitle} id="dialog-box-title">
${this._params.warning
? html`<ha-svg-icon
.path=${mdiAlertOutline}
@@ -88,13 +78,13 @@ class DialogBox extends LitElement {
: nothing}
${dialogTitle}
</span>
</ha-dialog-header>
<div id="dialog-box-description">
</div>
<div slot="content" id="dialog-box-description">
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
${this._params.prompt
? html`
<ha-textfield
autofocus
dialogInitialFocus
value=${ifDefined(this._params.defaultValue)}
.placeholder=${this._params.placeholder}
.label=${this._params.inputLabel
@@ -109,11 +99,10 @@ class DialogBox extends LitElement {
`
: ""}
</div>
<ha-dialog-footer slot="footer">
<div slot="actions">
${confirmPrompt
? html`
<ha-button
slot="secondaryAction"
@click=${this._dismiss}
?autofocus=${!this._params.prompt && this._params.destructive}
appearance="plain"
@@ -125,7 +114,6 @@ class DialogBox extends LitElement {
`
: nothing}
<ha-button
slot="primaryAction"
@click=${this._confirm}
?autofocus=${!this._params.prompt && !this._params.destructive}
variant=${this._params.destructive ? "danger" : "brand"}
@@ -134,8 +122,8 @@ class DialogBox extends LitElement {
? this._params.confirmText
: this.hass.localize("ui.common.ok")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
</div>
</ha-md-dialog>
`;
}
@@ -160,20 +148,20 @@ class DialogBox extends LitElement {
}
private _closeDialog() {
this._open = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._dialog?.close();
this._closePromise = new Promise((resolve) => {
this._closeResolve = resolve;
});
}
private _dialogClosed() {
fireEvent(this, "dialog-closed", { dialog: this.localName });
if (!this._closeState) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._cancel();
}
this._closeState = undefined;
this._params = undefined;
this._open = false;
this._closeResolve?.();
this._closeResolve = undefined;
}

View File

@@ -2,16 +2,16 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-alert";
import "../../components/ha-icon";
import "../../components/ha-md-list-item";
import "../../components/ha-list-item";
import "../../components/ha-spinner";
import type {
ExternalEntityAddToAction,
ExternalEntityAddToActions,
ExternalEntityAddToAction,
} from "../../external_app/external_messaging";
import { showToast } from "../../util/toast";
import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("ha-more-info-add-to")
export class HaMoreInfoAddTo extends LitElement {
@@ -93,18 +93,19 @@ export class HaMoreInfoAddTo extends LitElement {
<div class="actions-list">
${this._externalActions.actions.map(
(action) => html`
<ha-md-list-item
type="button"
<ha-list-item
graphic="icon"
.disabled=${!action.enabled}
.action=${action}
.twoline=${!!action.details}
@click=${this._actionSelected}
>
<ha-icon slot="start" .icon=${action.mdi_icon}></ha-icon>
<span>${action.name}</span>
${action.details
? html`<span slot="supporting-text">${action.details}</span>`
? html`<span slot="secondary">${action.details}</span>`
: nothing}
</ha-md-list-item>
<ha-icon slot="graphic" .icon=${action.mdi_icon}></ha-icon>
</ha-list-item>
`
)}
</div>
@@ -130,6 +131,15 @@ export class HaMoreInfoAddTo extends LitElement {
flex-direction: column;
}
ha-list-item {
cursor: pointer;
}
ha-list-item[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
ha-icon {
display: flex;
align-items: center;

View File

@@ -1,5 +1,5 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { mdiClose, mdiDotsVertical, mdiRestart } from "@mdi/js";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing, type TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -9,30 +9,18 @@ import "../../components/ha-dialog-header";
import "../../components/ha-fade-in";
import "../../components/ha-icon-button";
import "../../components/ha-items-display-editor";
import type {
DisplayItem,
DisplayValue,
} from "../../components/ha-items-display-editor";
import "../../components/ha-md-button-menu";
import type { DisplayValue } from "../../components/ha-items-display-editor";
import "../../components/ha-md-dialog";
import type { HaMdDialog } from "../../components/ha-md-dialog";
import "../../components/ha-md-menu-item";
import { computePanels } from "../../components/ha-sidebar";
import { computePanels, PANEL_ICONS } from "../../components/ha-sidebar";
import "../../components/ha-spinner";
import "../../components/ha-svg-icon";
import {
fetchFrontendUserData,
saveFrontendUserData,
} from "../../data/frontend";
import {
getDefaultPanelUrlPath,
getPanelIcon,
getPanelIconPath,
getPanelTitle,
SHOW_AFTER_SPACER_PANELS,
} from "../../data/panel";
import type { HomeAssistant } from "../../types";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import { getDefaultPanelUrlPath } from "../../data/panel";
@customElement("dialog-edit-sidebar")
class DialogEditSidebar extends LitElement {
@@ -117,53 +105,48 @@ class DialogEditSidebar extends LitElement {
this.hass.locale
);
const orderSet = new Set(this._order);
const hiddenSet = new Set(this._hidden);
// Add default hidden panels that are missing in hidden
for (const panel of panels) {
if (
panel.default_visible === false &&
!orderSet.has(panel.url_path) &&
!hiddenSet.has(panel.url_path)
!this._order.includes(panel.url_path) &&
!this._hidden.includes(panel.url_path)
) {
hiddenSet.add(panel.url_path);
this._hidden.push(panel.url_path);
}
}
if (hiddenSet.has(defaultPanel)) {
hiddenSet.delete(defaultPanel);
}
const hiddenPanels = Array.from(hiddenSet);
const items = [
...beforeSpacer,
...panels.filter((panel) => hiddenPanels.includes(panel.url_path)),
...afterSpacer,
].map<DisplayItem>((panel) => ({
...panels.filter((panel) => this._hidden!.includes(panel.url_path)),
...afterSpacer.filter((panel) => panel.url_path !== "config"),
].map((panel) => ({
value: panel.url_path,
label:
(getPanelTitle(this.hass, panel) || panel.url_path) +
`${defaultPanel === panel.url_path ? " (default)" : ""}`,
icon: getPanelIcon(panel),
iconPath: getPanelIconPath(panel),
disableSorting: SHOW_AFTER_SPACER_PANELS.includes(panel.url_path),
disableHiding: panel.url_path === defaultPanel,
panel.url_path === defaultPanel
? panel.title || this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) || panel.title || "?",
icon: panel.icon || undefined,
iconPath:
panel.url_path === defaultPanel && !panel.icon
? PANEL_ICONS.lovelace
: panel.url_path in PANEL_ICONS
? PANEL_ICONS[panel.url_path]
: undefined,
disableSorting: panel.url_path === "developer-tools",
}));
return html`
<ha-items-display-editor
.hass=${this.hass}
.value=${{
order: this._order,
hidden: hiddenPanels,
}}
.items=${items}
@value-changed=${this._changed}
dont-sort-visible
>
</ha-items-display-editor>
`;
return html`<ha-items-display-editor
.hass=${this.hass}
.value=${{
order: this._order,
hidden: this._hidden,
}}
.items=${items}
@value-changed=${this._changed}
dont-sort-visible
>
</ha-items-display-editor>`;
}
protected render() {
@@ -188,22 +171,6 @@ class DialogEditSidebar extends LitElement {
>${this.hass.localize("ui.sidebar.edit_subtitle")}</span
>`
: nothing}
<ha-md-button-menu
slot="actionItems"
positioning="popover"
anchor-corner="end-end"
menu-corner="start-end"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item .clickAction=${this._resetToDefaults}>
<ha-svg-icon slot="start" .path=${mdiRestart}></ha-svg-icon>
${this.hass.localize("ui.sidebar.reset_to_defaults")}
</ha-md-menu-item>
</ha-md-button-menu>
</ha-dialog-header>
<div slot="content" class="content">${this._renderContent()}</div>
<div slot="actions">
@@ -227,26 +194,6 @@ class DialogEditSidebar extends LitElement {
this._hidden = [...hidden];
}
private _resetToDefaults = async () => {
const confirmation = await showConfirmationDialog(this, {
text: this.hass.localize("ui.sidebar.reset_confirmation"),
confirmText: this.hass.localize("ui.common.reset"),
});
if (!confirmation) {
return;
}
this._order = [];
this._hidden = [];
try {
await saveFrontendUserData(this.hass.connection, "sidebar", {});
} catch (err: any) {
this._error = err.message || err;
}
this.closeDialog();
};
private async _save() {
if (this._migrateToUserData) {
const confirmation = await showConfirmationDialog(this, {

View File

@@ -7,7 +7,6 @@ import { listenMediaQuery } from "../common/dom/media_query";
import { toggleAttribute } from "../common/dom/toggle_attribute";
import { computeRTLDirection } from "../common/util/compute_rtl";
import "../components/ha-drawer";
import "../components/ha-snowflakes";
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
import type { HomeAssistant, Route } from "../types";
import "./partial-panel-resolver";
@@ -51,7 +50,6 @@ export class HomeAssistantMain extends LitElement {
this.hass.panels && this.hass.userData && this.hass.systemData;
return html`
<ha-snowflakes .hass=${this.hass} .narrow=${this.narrow}></ha-snowflakes>
<ha-drawer
.type=${sidebarNarrow ? "modal" : ""}
.open=${sidebarNarrow ? this._drawerOpen : false}

View File

@@ -1,187 +0,0 @@
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { css, html } from "lit";
import type {
CSSResultGroup,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { classMap } from "lit/directives/class-map";
import { state } from "lit/decorators";
import type { Constructor } from "../types";
const stylesArray = (styles?: CSSResultGroup | CSSResultGroup[]) =>
styles === undefined ? [] : Array.isArray(styles) ? styles : [styles];
export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
superClass: T
) => {
class ScrollableFadeClass extends superClass {
@state() protected _contentScrolled = false;
@state() protected _contentScrollable = false;
private _scrollTarget?: HTMLElement | null;
private _onScroll = (ev: Event) => {
const target = ev.currentTarget as HTMLElement;
this._contentScrolled = (target.scrollTop ?? 0) > 0;
this._updateScrollableState(target);
};
private _resize = new ResizeController(this, {
target: null,
callback: (entries) => {
const target = entries[0]?.target as HTMLElement | undefined;
if (target) {
this._updateScrollableState(target);
}
},
});
private static readonly DEFAULT_SAFE_AREA_PADDING = 16;
private static readonly DEFAULT_SCROLLABLE_ELEMENT: HTMLElement | null =
null;
protected get scrollFadeSafeAreaPadding() {
return ScrollableFadeClass.DEFAULT_SAFE_AREA_PADDING;
}
protected get scrollableElement(): HTMLElement | null {
return ScrollableFadeClass.DEFAULT_SCROLLABLE_ELEMENT;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated?.(changedProperties);
this._attachScrollableElement();
}
protected updated(changedProperties: PropertyValues) {
super.updated?.(changedProperties);
this._attachScrollableElement();
}
disconnectedCallback() {
this._detachScrollableElement();
super.disconnectedCallback();
}
protected renderScrollableFades(rounded = false): TemplateResult {
return html`
<div
class=${classMap({
"fade-top": true,
rounded,
visible: this._contentScrolled,
})}
></div>
<div
class=${classMap({
"fade-bottom": true,
rounded,
visible: this._contentScrollable,
})}
></div>
`;
}
static get styles() {
const superCtor = Object.getPrototypeOf(this) as
| typeof LitElement
| undefined;
const inheritedStyles = stylesArray(
(superCtor?.styles ?? []) as CSSResultGroup | CSSResultGroup[]
);
return [
...inheritedStyles,
css`
.fade-top,
.fade-bottom {
position: absolute;
left: var(--ha-space-0);
right: var(--ha-space-0);
height: var(--ha-space-4);
pointer-events: none;
transition: opacity 180ms ease-in-out;
background: linear-gradient(
to bottom,
var(--shadow-color),
transparent
);
border-radius: var(--ha-border-radius-square);
z-index: 100;
opacity: 0;
}
.fade-top {
top: var(--ha-space-0);
}
.fade-bottom {
bottom: var(--ha-space-0);
transform: rotate(180deg);
}
.fade-top.visible,
.fade-bottom.visible {
opacity: 1;
}
.fade-top.rounded,
.fade-bottom.rounded {
border-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
border-bottom-left-radius: var(--ha-border-radius-square);
border-bottom-right-radius: var(--ha-border-radius-square);
}
.fade-top.rounded {
border-top-left-radius: var(--ha-border-radius-square);
border-top-right-radius: var(--ha-border-radius-square);
}
.fade-bottom.rounded {
border-bottom-left-radius: var(--ha-border-radius-square);
border-bottom-right-radius: var(--ha-border-radius-square);
}
`,
];
}
private _attachScrollableElement() {
const element = this.scrollableElement;
if (element === this._scrollTarget) {
return;
}
this._detachScrollableElement();
if (!element) {
return;
}
this._scrollTarget = element;
element.addEventListener("scroll", this._onScroll, { passive: true });
this._resize.observe(element);
this._updateScrollableState(element);
}
private _detachScrollableElement() {
if (!this._scrollTarget) {
return;
}
this._scrollTarget.removeEventListener("scroll", this._onScroll);
this._resize.unobserve?.(this._scrollTarget);
this._scrollTarget = undefined;
}
private _updateScrollableState(element: HTMLElement) {
const safeAreaInsetBottom =
parseFloat(
getComputedStyle(element).getPropertyValue("--safe-area-inset-bottom")
) || 0;
const { scrollHeight = 0, clientHeight = 0, scrollTop = 0 } = element;
this._contentScrollable =
scrollHeight - clientHeight >
scrollTop + safeAreaInsetBottom + this.scrollFadeSafeAreaPadding;
}
}
return ScrollableFadeClass;
};

View File

@@ -87,7 +87,7 @@ class PanelCalendar extends LitElement {
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._calendars = getCalendars(this.hass, this);
this._calendars = getCalendars(this.hass);
}
}
@@ -243,7 +243,7 @@ class PanelCalendar extends LitElement {
manifest: await fetchIntegrationManifest(this.hass, "local_calendar"),
dialogClosedCallback: ({ flowFinished }) => {
if (flowFinished) {
this._calendars = getCalendars(this.hass, this);
this._calendars = getCalendars(this.hass);
}
},
});

View File

@@ -1,29 +1,19 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
import { stringCompare } from "../../../../../common/string/compare";
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import { CONDITION_ICONS } from "../../../../../components/ha-condition-icon";
import "../../../../../components/ha-list-item";
import "../../../../../components/ha-select";
import type { HaSelect } from "../../../../../components/ha-select";
import {
DYNAMIC_PREFIX,
getValueFromDynamic,
isDynamic,
type Condition,
} from "../../../../../data/automation";
import type { ConditionDescriptions } from "../../../../../data/condition";
import type { Condition } from "../../../../../data/automation";
import {
CONDITION_BUILDING_BLOCKS,
getConditionDomain,
getConditionObjectId,
subscribeConditions,
CONDITION_ICONS,
} from "../../../../../data/condition";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../../types";
import type { Entries, HomeAssistant } from "../../../../../types";
import "../../condition/ha-automation-condition-editor";
import type HaAutomationConditionEditor from "../../condition/ha-automation-condition-editor";
import "../../condition/types/ha-automation-condition-and";
@@ -40,10 +30,7 @@ import "../../condition/types/ha-automation-condition-zone";
import type { ActionElement } from "../ha-automation-action-row";
@customElement("ha-automation-action-condition")
export class HaConditionAction
extends SubscribeMixin(LitElement)
implements ActionElement
{
export class HaConditionAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public disabled = false;
@@ -56,8 +43,6 @@ export class HaConditionAction
@property({ type: Boolean, attribute: "indent" }) public indent = false;
@state() private _conditionDescriptions: ConditionDescriptions = {};
@query("ha-automation-condition-editor")
private _conditionEditor?: HaAutomationConditionEditor;
@@ -65,21 +50,6 @@ export class HaConditionAction
return { condition: "state" };
}
protected hassSubscribe() {
return [
subscribeConditions(this.hass, (conditions) =>
this._addConditions(conditions)
),
];
}
private _addConditions(conditions: ConditionDescriptions) {
this._conditionDescriptions = {
...this._conditionDescriptions,
...conditions,
};
}
protected render() {
const buildingBlock = CONDITION_BUILDING_BLOCKS.includes(
this.action.condition
@@ -94,25 +64,19 @@ export class HaConditionAction
"ui.panel.config.automation.editor.conditions.type_select"
)}
.disabled=${this.disabled}
.value=${this.action.condition in this._conditionDescriptions
? `${DYNAMIC_PREFIX}${this.action.condition}`
: this.action.condition}
.value=${this.action.condition}
naturalMenuWidth
@selected=${this._typeChanged}
@closed=${stopPropagation}
>
${this._processedTypes(
this._conditionDescriptions,
this.hass.localize
).map(
([opt, label, condition]) => html`
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`
<ha-list-item .value=${opt} graphic="icon">
${label}
<ha-condition-icon
${label}<ha-svg-icon
slot="graphic"
.condition=${condition}
></ha-condition-icon>
</ha-list-item>
.path=${icon}
></ha-svg-icon
></ha-list-item>
`
)}
</ha-select>
@@ -124,14 +88,11 @@ export class HaConditionAction
? html`
<ha-automation-condition-editor
.condition=${this.action}
.description=${this._conditionDescriptions[this.action.condition]}
.disabled=${this.disabled}
.hass=${this.hass}
@value-changed=${this._conditionChanged}
.narrow=${this.narrow}
.uiSupported=${this._uiSupported(
this._getType(this.action, this._conditionDescriptions)
)}
.uiSupported=${this._uiSupported(this.action.condition)}
.indent=${this.indent}
action
></ha-automation-condition-editor>
@@ -141,46 +102,19 @@ export class HaConditionAction
}
private _processedTypes = memoizeOne(
(
conditionDescriptions: ConditionDescriptions,
localize: LocalizeFunc
): [string, string, string][] => {
const legacy = (
Object.keys(CONDITION_ICONS) as (keyof typeof CONDITION_ICONS)[]
).map(
(condition) =>
[
condition,
localize(
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
),
condition,
] as [string, string, string]
);
const platform = Object.keys(conditionDescriptions).map((condition) => {
const domain = getConditionDomain(condition);
const conditionObjId = getConditionObjectId(condition);
return [
`${DYNAMIC_PREFIX}${condition}`,
localize(`component.${domain}.conditions.${conditionObjId}.name`) ||
condition,
condition,
] as [string, string, string];
});
return [...legacy, ...platform].sort((a, b) =>
stringCompare(a[1], b[1], this.hass.locale.language)
);
}
);
private _getType = memoizeOne(
(condition: Condition, conditionDescriptions: ConditionDescriptions) => {
if (condition.condition in conditionDescriptions) {
return "platform";
}
return condition.condition;
}
(localize: LocalizeFunc): [string, string, string][] =>
(Object.entries(CONDITION_ICONS) as Entries<typeof CONDITION_ICONS>)
.map(
([condition, icon]) =>
[
condition,
localize(
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
),
icon,
] as [string, string, string]
)
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
);
private _conditionChanged(ev: CustomEvent) {
@@ -198,18 +132,6 @@ export class HaConditionAction
return;
}
if (isDynamic(type)) {
const value = getValueFromDynamic(type);
if (value !== this.action.condition) {
fireEvent(this, "value-changed", {
value: {
condition: value,
},
});
}
return;
}
const elClass = customElements.get(
`ha-automation-condition-${type}`
) as CustomElementConstructor & {

View File

@@ -56,19 +56,12 @@ import {
type AutomationElementGroup,
type AutomationElementGroupCollection,
} from "../../../data/automation";
import type { ConditionDescriptions } from "../../../data/condition";
import {
CONDITION_BUILDING_BLOCKS_GROUP,
CONDITION_COLLECTIONS,
getConditionDomain,
getConditionObjectId,
subscribeConditions,
CONDITION_ICONS,
} from "../../../data/condition";
import {
getConditionIcons,
getServiceIcons,
getTriggerIcons,
} from "../../../data/icons";
import { getServiceIcons, getTriggerIcons } from "../../../data/icons";
import type { IntegrationManifest } from "../../../data/integration";
import {
domainToName,
@@ -89,7 +82,6 @@ import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
import { CONDITION_ICONS } from "../../../components/ha-condition-icon";
const TYPES = {
trigger: { collections: TRIGGER_COLLECTIONS, icons: TRIGGER_ICONS },
@@ -127,7 +119,7 @@ const ENTITY_DOMAINS_OTHER = new Set([
const ENTITY_DOMAINS_MAIN = new Set(["notify"]);
const DYNAMIC_KEYWORDS = ["dynamicGroups", "helpers", "other"];
const ACTION_SERVICE_KEYWORDS = ["dynamicGroups", "helpers", "other"];
@customElement("add-automation-element-dialog")
class DialogAddAutomationElement
@@ -160,8 +152,6 @@ class DialogAddAutomationElement
@state() private _triggerDescriptions: TriggerDescriptions = {};
@state() private _conditionDescriptions: ConditionDescriptions = {};
@query(".items ha-md-list ha-md-list-item")
private _itemsListFirstElement?: HaMdList;
@@ -179,15 +169,15 @@ class DialogAddAutomationElement
this.addKeyboardShortcuts();
this._unsubscribe();
this._fetchManifests();
if (this._params?.type === "action") {
this.hass.loadBackendTranslation("services");
this._fetchManifests();
this._calculateUsedDomains();
getServiceIcons(this.hass);
} else if (this._params?.type === "trigger") {
}
if (this._params?.type === "trigger") {
this.hass.loadBackendTranslation("triggers");
this._fetchManifests();
getTriggerIcons(this.hass);
this._unsub = subscribeTriggers(this.hass, (triggers) => {
this._triggerDescriptions = {
@@ -195,17 +185,7 @@ class DialogAddAutomationElement
...triggers,
};
});
} else if (this._params?.type === "condition") {
this.hass.loadBackendTranslation("conditions");
getConditionIcons(this.hass);
this._unsub = subscribeConditions(this.hass, (conditions) => {
this._conditionDescriptions = {
...this._conditionDescriptions,
...conditions,
};
});
}
this._fullScreen = matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
@@ -219,7 +199,10 @@ class DialogAddAutomationElement
public closeDialog() {
this.removeKeyboardShortcuts();
this._unsubscribe();
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
if (this._params) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -236,13 +219,6 @@ class DialogAddAutomationElement
return true;
}
private _unsubscribe() {
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
}
private _getGroups = (
type: AddAutomationElementDialogParams["type"],
group?: string,
@@ -372,11 +348,8 @@ class DialogAddAutomationElement
items.push(
...this._triggers(localize, this._triggerDescriptions, manifests)
);
} else if (type === "condition") {
items.push(
...this._conditions(localize, this._conditionDescriptions, manifests)
);
} else if (type === "action") {
}
if (type === "action") {
items.push(...this._services(localize, services, manifests));
}
return items;
@@ -399,7 +372,6 @@ class DialogAddAutomationElement
localize: LocalizeFunc,
services: HomeAssistant["services"],
triggerDescriptions: TriggerDescriptions,
conditionDescriptions: ConditionDescriptions,
manifests?: DomainManifestLookup
): {
titleKey?: LocalizeKeys;
@@ -412,55 +384,9 @@ class DialogAddAutomationElement
const groups: ListItem[] = [];
if (
type === "trigger" &&
Object.keys(collection.groups).some((item) =>
DYNAMIC_KEYWORDS.includes(item)
)
) {
groups.push(
...this._triggerGroups(
localize,
triggerDescriptions,
manifests,
domains,
collection.groups.dynamicGroups
? undefined
: collection.groups.helpers
? "helper"
: "other"
)
);
collectionGroups = collectionGroups.filter(
([key]) => !DYNAMIC_KEYWORDS.includes(key)
);
} else if (
type === "condition" &&
Object.keys(collection.groups).some((item) =>
DYNAMIC_KEYWORDS.includes(item)
)
) {
groups.push(
...this._conditionGroups(
localize,
conditionDescriptions,
manifests,
domains,
collection.groups.dynamicGroups
? undefined
: collection.groups.helpers
? "helper"
: "other"
)
);
collectionGroups = collectionGroups.filter(
([key]) => !DYNAMIC_KEYWORDS.includes(key)
);
} else if (
type === "action" &&
Object.keys(collection.groups).some((item) =>
DYNAMIC_KEYWORDS.includes(item)
ACTION_SERVICE_KEYWORDS.includes(item)
)
) {
groups.push(
@@ -478,7 +404,32 @@ class DialogAddAutomationElement
);
collectionGroups = collectionGroups.filter(
([key]) => !DYNAMIC_KEYWORDS.includes(key)
([key]) => !ACTION_SERVICE_KEYWORDS.includes(key)
);
}
if (
type === "trigger" &&
Object.keys(collection.groups).some((item) =>
ACTION_SERVICE_KEYWORDS.includes(item)
)
) {
groups.push(
...this._triggerGroups(
localize,
triggerDescriptions,
manifests,
domains,
collection.groups.dynamicGroups
? undefined
: collection.groups.helpers
? "helper"
: "other"
)
);
collectionGroups = collectionGroups.filter(
([key]) => !ACTION_SERVICE_KEYWORDS.includes(key)
);
}
@@ -536,6 +487,10 @@ class DialogAddAutomationElement
services: HomeAssistant["services"],
manifests?: DomainManifestLookup
): ListItem[] => {
if (type === "action" && isDynamic(group)) {
return this._services(localize, services, manifests, group);
}
if (type === "trigger" && isDynamic(group)) {
return this._triggers(
localize,
@@ -544,17 +499,6 @@ class DialogAddAutomationElement
group
);
}
if (type === "condition" && isDynamic(group)) {
return this._conditions(
localize,
this._conditionDescriptions,
manifests,
group
);
}
if (type === "action" && isDynamic(group)) {
return this._services(localize, services, manifests, group);
}
const groups = this._getGroups(type, group, collectionIndex);
@@ -744,102 +688,6 @@ class DialogAddAutomationElement
}
);
private _conditionGroups = (
localize: LocalizeFunc,
conditions: ConditionDescriptions,
manifests: DomainManifestLookup | undefined,
domains: Set<string> | undefined,
type: "helper" | "other" | undefined
): ListItem[] => {
if (!conditions || !manifests) {
return [];
}
const result: ListItem[] = [];
const addedDomains = new Set<string>();
Object.keys(conditions).forEach((condition) => {
const domain = getConditionDomain(condition);
if (addedDomains.has(domain)) {
return;
}
addedDomains.add(domain);
const manifest = manifests[domain];
const domainUsed = !domains ? true : domains.has(domain);
if (
(type === undefined &&
(ENTITY_DOMAINS_MAIN.has(domain) ||
(manifest?.integration_type === "entity" &&
domainUsed &&
!ENTITY_DOMAINS_OTHER.has(domain)))) ||
(type === "helper" && manifest?.integration_type === "helper") ||
(type === "other" &&
!ENTITY_DOMAINS_MAIN.has(domain) &&
(ENTITY_DOMAINS_OTHER.has(domain) ||
(!domainUsed && manifest?.integration_type === "entity") ||
!["helper", "entity"].includes(manifest?.integration_type || "")))
) {
result.push({
icon: html`
<ha-domain-icon
.hass=${this.hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>
`,
key: `${DYNAMIC_PREFIX}${domain}`,
name: domainToName(localize, domain, manifest),
description: "",
});
}
});
return result.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
};
private _conditions = memoizeOne(
(
localize: LocalizeFunc,
conditions: ConditionDescriptions,
_manifests: DomainManifestLookup | undefined,
group?: string
): ListItem[] => {
if (!conditions) {
return [];
}
const result: ListItem[] = [];
for (const condition of Object.keys(conditions)) {
const domain = getConditionDomain(condition);
const conditionName = getConditionObjectId(condition);
if (group && group !== `${DYNAMIC_PREFIX}${domain}`) {
continue;
}
result.push({
icon: html`
<ha-condition-icon
.hass=${this.hass}
.condition=${condition}
></ha-condition-icon>
`,
key: `${DYNAMIC_PREFIX}${condition}`,
name:
localize(`component.${domain}.conditions.${conditionName}.name`) ||
condition,
description:
localize(
`component.${domain}.conditions.${conditionName}.description`
) || condition,
});
}
return result;
}
);
private _services = memoizeOne(
(
localize: LocalizeFunc,
@@ -871,17 +719,13 @@ class DialogAddAutomationElement
`,
key: `${DYNAMIC_PREFIX}${dmn}.${service}`,
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
this.hass.localize(
`component.${dmn}.services.${service}.name`,
this.hass.services[dmn][service].description_placeholders
) ||
this.hass.localize(`component.${dmn}.services.${service}.name`) ||
services[dmn][service]?.name ||
service
}`,
description:
this.hass.localize(
`component.${dmn}.services.${service}.description`,
this.hass.services[dmn][service].description_placeholders
`component.${dmn}.services.${service}.description`
) ||
services[dmn][service]?.description ||
"",
@@ -988,7 +832,6 @@ class DialogAddAutomationElement
this.hass.localize,
this.hass.services,
this._triggerDescriptions,
this._conditionDescriptions,
this._manifests
);
@@ -1293,7 +1136,6 @@ class DialogAddAutomationElement
super.disconnectedCallback();
window.removeEventListener("resize", this._updateNarrow);
this._removeSearchKeybindings();
this._unsubscribe();
}
private _updateNarrow = () => {

View File

@@ -8,13 +8,11 @@ import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import type { Condition } from "../../../../data/automation";
import { expandConditionWithShorthand } from "../../../../data/automation";
import type { ConditionDescription } from "../../../../data/condition";
import { COLLAPSIBLE_CONDITION_ELEMENTS } from "../../../../data/condition";
import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning";
import { editorStyles, indentStyle } from "../styles";
import type { ConditionElement } from "./ha-automation-condition-row";
import "./types/ha-automation-condition-platform";
@customElement("ha-automation-condition-editor")
export default class HaAutomationConditionEditor extends LitElement {
@@ -37,8 +35,6 @@ export default class HaAutomationConditionEditor extends LitElement {
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
false;
@property({ attribute: false }) public description?: ConditionDescription;
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
@query(COLLAPSIBLE_CONDITION_ELEMENTS.join(", "))
@@ -87,23 +83,16 @@ export default class HaAutomationConditionEditor extends LitElement {
`
: html`
<div @value-changed=${this._onUiChanged}>
${this.description
? html`<ha-automation-condition-platform
.hass=${this.hass}
.condition=${this.condition}
.description=${this.description}
.disabled=${this.disabled}
></ha-automation-condition-platform>`
: dynamicElement(
`ha-automation-condition-${condition.condition}`,
{
hass: this.hass,
condition: condition,
disabled: this.disabled,
optionsInSidebar: this.indent,
narrow: this.narrow,
}
)}
${dynamicElement(
`ha-automation-condition-${condition.condition}`,
{
hass: this.hass,
condition: condition,
disabled: this.disabled,
optionsInSidebar: this.indent,
narrow: this.narrow,
}
)}
</div>
`}
</div>

View File

@@ -32,7 +32,6 @@ import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-automation-row";
import type { HaAutomationRow } from "../../../../components/ha-automation-row";
import "../../../../components/ha-card";
import "../../../../components/ha-condition-icon";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu";
@@ -45,8 +44,10 @@ import type {
} from "../../../../data/automation";
import { isCondition, testCondition } from "../../../../data/automation";
import { describeCondition } from "../../../../data/automation_i18n";
import type { ConditionDescriptions } from "../../../../data/condition";
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
import {
CONDITION_BUILDING_BLOCKS,
CONDITION_ICONS,
} from "../../../../data/condition";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
@@ -129,9 +130,6 @@ export default class HaAutomationConditionRow extends LitElement {
@state() private _warnings?: string[];
@property({ attribute: false })
public conditionDescriptions: ConditionDescriptions = {};
@property({ type: Boolean, attribute: "sidebar" })
public optionsInSidebar = false;
@@ -181,11 +179,11 @@ export default class HaAutomationConditionRow extends LitElement {
private _renderRow() {
return html`
<ha-condition-icon
<ha-svg-icon
slot="leading-icon"
.hass=${this.hass}
.condition=${this.condition.condition}
></ha-condition-icon>
class="condition-icon"
.path=${CONDITION_ICONS[this.condition.condition]}
></ha-svg-icon>
<h3 slot="header">
${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)
@@ -397,14 +395,9 @@ export default class HaAutomationConditionRow extends LitElement {
<ha-automation-condition-editor
.hass=${this.hass}
.condition=${this.condition}
.description=${this.conditionDescriptions[
this.condition.condition
]}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
.uiSupported=${this._uiSupported(
this._getType(this.condition, this.conditionDescriptions)
)}
.uiSupported=${this._uiSupported(this.condition.condition)}
.narrow=${this.narrow}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-condition-editor>`
@@ -483,9 +476,7 @@ export default class HaAutomationConditionRow extends LitElement {
.hass=${this.hass}
.condition=${this.condition}
.disabled=${this.disabled}
.uiSupported=${this._uiSupported(
this._getType(this.condition, this.conditionDescriptions)
)}
.uiSupported=${this._uiSupported(this.condition.condition)}
indent
.selected=${this._selected}
.narrow=${this.narrow}
@@ -795,10 +786,7 @@ export default class HaAutomationConditionRow extends LitElement {
cut: this._cutCondition,
test: this._testCondition,
config: sidebarCondition,
uiSupported: this._uiSupported(
this._getType(sidebarCondition, this.conditionDescriptions)
),
description: this.conditionDescriptions[sidebarCondition.condition],
uiSupported: this._uiSupported(sidebarCondition.condition),
yamlMode: this._yamlMode,
} satisfies ConditionSidebarConfig);
this._selected = true;
@@ -814,16 +802,6 @@ export default class HaAutomationConditionRow extends LitElement {
}
}
private _getType = memoizeOne(
(condition: Condition, conditionDescriptions: ConditionDescriptions) => {
if (condition.condition in conditionDescriptions) {
return "platform";
}
return condition.condition;
}
);
private _uiSupported = memoizeOne(
(type: string) =>
customElements.get(`ha-automation-condition-${type}`) !== undefined

View File

@@ -4,7 +4,6 @@ import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { ensureArray } from "../../../../common/array/ensure-array";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
@@ -13,18 +12,11 @@ import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import {
getValueFromDynamic,
isDynamic,
type AutomationClipboard,
type Condition,
import type {
AutomationClipboard,
Condition,
} from "../../../../data/automation";
import type { ConditionDescriptions } from "../../../../data/condition";
import {
CONDITION_BUILDING_BLOCKS,
subscribeConditions,
} from "../../../../data/condition";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
import type { HomeAssistant } from "../../../../types";
import {
PASTE_VALUE,
@@ -33,9 +25,10 @@ import {
import { automationRowsStyles } from "../styles";
import "./ha-automation-condition-row";
import type HaAutomationConditionRow from "./ha-automation-condition-row";
import { ensureArray } from "../../../../common/array/ensure-array";
@customElement("ha-automation-condition")
export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
export default class HaAutomationCondition extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public conditions!: Condition[];
@@ -53,8 +46,6 @@ export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
@state() private _rowSortSelected?: number;
@state() private _conditionDescriptions: ConditionDescriptions = {};
@state()
@storage({
key: "automationClipboard",
@@ -73,26 +64,6 @@ export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
private _conditionKeys = new WeakMap<Condition, string>();
protected hassSubscribe() {
return [
subscribeConditions(this.hass, (conditions) =>
this._addConditions(conditions)
),
];
}
private _addConditions(conditions: ConditionDescriptions) {
this._conditionDescriptions = {
...this._conditionDescriptions,
...conditions,
};
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.hass.loadBackendTranslation("conditions");
}
protected updated(changedProperties: PropertyValues) {
if (!changedProperties.has("conditions")) {
return;
@@ -197,7 +168,6 @@ export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
.last=${idx === this.conditions.length - 1}
.totalConditions=${this.conditions.length}
.condition=${cond}
.conditionDescriptions=${this._conditionDescriptions}
.disabled=${this.disabled}
.narrow=${this.narrow}
@duplicate=${this._duplicateCondition}
@@ -267,10 +237,6 @@ export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
conditions = this.conditions.concat(
deepClone(this._clipboard!.condition)
);
} else if (isDynamic(value)) {
conditions = this.conditions.concat({
condition: getValueFromDynamic(value),
});
} else {
const condition = value as Condition["condition"];
const elClass = customElements.get(

View File

@@ -1,416 +0,0 @@
import { mdiHelpCircle } from "@mdi/js";
import type { PropertyValues } from "lit";
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";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import "../../../../../components/ha-checkbox";
import "../../../../../components/ha-selector/ha-selector";
import "../../../../../components/ha-settings-row";
import type { PlatformCondition } from "../../../../../data/automation";
import {
getConditionDomain,
getConditionObjectId,
type ConditionDescription,
} from "../../../../../data/condition";
import type { IntegrationManifest } from "../../../../../data/integration";
import { fetchIntegrationManifest } from "../../../../../data/integration";
import type { TargetSelector } from "../../../../../data/selector";
import type { HomeAssistant } from "../../../../../types";
import { documentationUrl } from "../../../../../util/documentation-url";
const showOptionalToggle = (field: ConditionDescription["fields"][string]) =>
field.selector &&
!field.required &&
!("boolean" in field.selector && field.default);
@customElement("ha-automation-condition-platform")
export class HaPlatformCondition extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public condition!: PlatformCondition;
@property({ attribute: false }) public description?: ConditionDescription;
@property({ type: Boolean }) public disabled = false;
@state() private _checkedKeys = new Set();
@state() private _manifest?: IntegrationManifest;
public static get defaultConfig(): PlatformCondition {
return { condition: "" };
}
protected willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
if (!this.hasUpdated) {
this.hass.loadBackendTranslation("conditions");
this.hass.loadBackendTranslation("selector");
}
if (!changedProperties.has("condition")) {
return;
}
const oldValue = changedProperties.get("condition") as
| undefined
| this["condition"];
// Fetch the manifest if we have a condition selected and the condition domain changed.
// If no condition is selected, clear the manifest.
if (this.condition?.condition) {
const domain = getConditionDomain(this.condition.condition);
const oldDomain = getConditionDomain(oldValue?.condition || "");
if (domain !== oldDomain) {
this._fetchManifest(domain);
}
} else {
this._manifest = undefined;
}
}
protected render() {
const domain = getConditionDomain(this.condition.condition);
const conditionName = getConditionObjectId(this.condition.condition);
const description = this.hass.localize(
`component.${domain}.conditions.${conditionName}.description`
);
const conditionDesc = this.description;
const shouldRenderDataYaml = !conditionDesc?.fields;
const hasOptional = Boolean(
conditionDesc?.fields &&
Object.values(conditionDesc.fields).some((field) =>
showOptionalToggle(field)
)
);
return html`
<div class="description">
${description ? html`<p>${description}</p>` : nothing}
${this._manifest
? html`<a
href=${this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._manifest.domain}`
)
: this._manifest.documentation}
title=${this.hass.localize(
"ui.components.service-control.integration_doc"
)}
target="_blank"
rel="noreferrer"
>
<ha-icon-button
.path=${mdiHelpCircle}
class="help-icon"
></ha-icon-button>
</a>`
: nothing}
</div>
${conditionDesc && "target" in conditionDesc
? html`<ha-settings-row narrow>
${hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: nothing}
<span slot="heading"
>${this.hass.localize(
"ui.components.service-control.target"
)}</span
>
<span slot="description"
>${this.hass.localize(
"ui.components.service-control.target_secondary"
)}</span
><ha-selector
.hass=${this.hass}
.selector=${this._targetSelector(conditionDesc.target)}
.disabled=${this.disabled}
@value-changed=${this._targetChanged}
.value=${this.condition?.target}
></ha-selector
></ha-settings-row>`
: nothing}
${shouldRenderDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.components.service-control.action_data"
)}
.name=${"data"}
.readOnly=${this.disabled}
.defaultValue=${this.condition?.options}
@value-changed=${this._dataChanged}
></ha-yaml-editor>`
: Object.entries(conditionDesc.fields).map(([fieldName, dataField]) =>
this._renderField(
fieldName,
dataField,
hasOptional,
domain,
conditionName
)
)}
`;
}
private _targetSelector = memoizeOne(
(targetSelector: TargetSelector["target"] | null | undefined) =>
targetSelector ? { target: { ...targetSelector } } : { target: {} }
);
private _renderField = (
fieldName: string,
dataField: ConditionDescription["fields"][string],
hasOptional: boolean,
domain: string | undefined,
conditionName: string | undefined
) => {
const selector = dataField?.selector ?? { text: null };
const showOptional = showOptionalToggle(dataField);
return dataField.selector
? html`<ha-settings-row narrow>
${!showOptional
? hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: nothing
: html`<ha-checkbox
.key=${fieldName}
.checked=${this._checkedKeys.has(fieldName) ||
(this.condition?.options &&
this.condition.options[fieldName] !== undefined)}
.disabled=${this.disabled}
@change=${this._checkboxChanged}
slot="prefix"
></ha-checkbox>`}
<span slot="heading"
>${this.hass.localize(
`component.${domain}.conditions.${conditionName}.fields.${fieldName}.name`
) || conditionName}</span
>
<span slot="description"
>${this.hass.localize(
`component.${domain}.conditions.${conditionName}.fields.${fieldName}.description`
)}</span
>
<ha-selector
.disabled=${this.disabled ||
(showOptional &&
!this._checkedKeys.has(fieldName) &&
(!this.condition?.options ||
this.condition.options[fieldName] === undefined))}
.hass=${this.hass}
.selector=${selector}
.context=${this._generateContext(dataField)}
.key=${fieldName}
@value-changed=${this._dataChanged}
.value=${this.condition?.options
? this.condition.options[fieldName]
: undefined}
.placeholder=${dataField.default}
.localizeValue=${this._localizeValueCallback}
></ha-selector>
</ha-settings-row>`
: nothing;
};
private _generateContext(
field: ConditionDescription["fields"][string]
): Record<string, any> | undefined {
if (!field.context) {
return undefined;
}
const context = {};
for (const [context_key, data_key] of Object.entries(field.context)) {
context[context_key] =
data_key === "target"
? this.condition.target
: this.condition.options?.[data_key];
}
return context;
}
private _dataChanged(ev: CustomEvent) {
ev.stopPropagation();
if (ev.detail.isValid === false) {
// Don't clear an object selector that returns invalid YAML
return;
}
const key = (ev.currentTarget as any).key;
const value = ev.detail.value;
if (
this.condition?.options?.[key] === value ||
((!this.condition?.options || !(key in this.condition.options)) &&
(value === "" || value === undefined))
) {
return;
}
const options = { ...this.condition?.options, [key]: value };
if (
value === "" ||
value === undefined ||
(typeof value === "object" && !Object.keys(value).length)
) {
delete options[key];
}
fireEvent(this, "value-changed", {
value: {
...this.condition,
options,
},
});
}
private _targetChanged(ev: CustomEvent): void {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
...this.condition,
target: ev.detail.value,
},
});
}
private _checkboxChanged(ev) {
const checked = ev.currentTarget.checked;
const key = ev.currentTarget.key;
let options;
if (checked) {
this._checkedKeys.add(key);
const field =
this.description &&
Object.entries(this.description).find(([k, _value]) => k === key)?.[1];
let defaultValue = field?.default;
if (
defaultValue == null &&
field?.selector &&
"constant" in field.selector
) {
defaultValue = field.selector.constant?.value;
}
if (
defaultValue == null &&
field?.selector &&
"boolean" in field.selector
) {
defaultValue = false;
}
if (defaultValue != null) {
options = {
...this.condition?.options,
[key]: defaultValue,
};
}
} else {
this._checkedKeys.delete(key);
options = { ...this.condition?.options };
delete options[key];
}
if (options) {
fireEvent(this, "value-changed", {
value: {
...this.condition,
options,
},
});
}
this.requestUpdate("_checkedKeys");
}
private _localizeValueCallback = (key: string) => {
if (!this.condition?.condition) {
return "";
}
return this.hass.localize(
`component.${computeDomain(this.condition.condition)}.selector.${key}`
);
};
private async _fetchManifest(integration: string) {
this._manifest = undefined;
try {
this._manifest = await fetchIntegrationManifest(this.hass, integration);
} catch (_err: any) {
// eslint-disable-next-line no-console
console.log(`Unable to fetch integration manifest for ${integration}`);
// Ignore if loading manifest fails. Probably bad JSON in manifest
}
}
static styles = css`
ha-settings-row {
padding: 0 var(--ha-space-4);
}
ha-settings-row[narrow] {
padding-bottom: var(--ha-space-2);
}
ha-settings-row {
--settings-row-content-width: 100%;
--settings-row-prefix-display: contents;
border-top: var(
--service-control-items-border-top,
1px solid var(--divider-color)
);
}
ha-service-picker,
ha-entity-picker,
ha-yaml-editor {
display: block;
margin: 0 var(--ha-space-4);
}
ha-yaml-editor {
padding: var(--ha-space-4) 0;
}
p {
margin: 0 var(--ha-space-4);
padding: var(--ha-space-4) 0;
}
:host([hide-picker]) p {
padding-top: 0;
}
.checkbox-spacer {
width: 32px;
}
ha-checkbox {
margin-left: calc(var(--ha-space-4) * -1);
margin-inline-start: calc(var(--ha-space-4) * -1);
margin-inline-end: initial;
}
.help-icon {
color: var(--secondary-text-color);
}
.description {
justify-content: space-between;
display: flex;
align-items: center;
padding-right: 2px;
padding-inline-end: 2px;
padding-inline-start: initial;
}
.description p {
direction: ltr;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-condition-platform": HaPlatformCondition;
}
}

View File

@@ -299,6 +299,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
},
area: {
title: localize("ui.panel.config.automation.picker.headers.area"),
defaultHidden: true,
groupable: true,
filterable: true,
sortable: true,

View File

@@ -93,12 +93,8 @@ export default class HaAutomationSidebarAction extends LitElement {
".",
2
);
title = `${domainToName(this.hass.localize, domain)}: ${
this.hass.localize(
`component.${domain}.services.${service}.name`,
this.hass.services[domain][service].description_placeholders
) ||
this.hass.localize(`component.${domain}.services.${service}.name`) ||
this.hass.services[domain][service]?.name ||
title
}`;

View File

@@ -1,6 +1,14 @@
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { mdiClose, mdiDotsVertical } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import {
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
@@ -9,10 +17,8 @@ import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider";
import { haStyleScrollbar } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning";
import { ScrollableFadeMixin } from "../../../../mixins/scrollable-fade-mixin";
export interface SidebarOverflowMenuEntry {
clickAction: () => void;
@@ -25,9 +31,7 @@ export interface SidebarOverflowMenuEntry {
export type SidebarOverflowMenu = (SidebarOverflowMenuEntry | "separator")[];
@customElement("ha-automation-sidebar-card")
export default class HaAutomationSidebarCard extends ScrollableFadeMixin(
LitElement
) {
export default class HaAutomationSidebarCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
@@ -38,10 +42,23 @@ export default class HaAutomationSidebarCard extends ScrollableFadeMixin(
@property({ type: Boolean }) public narrow = false;
@state() private _contentScrolled = false;
@state() private _contentScrollable = false;
@query(".card-content") private _contentElement!: HTMLDivElement;
protected get scrollableElement(): HTMLElement | null {
return this._contentElement;
private _contentSize = new ResizeController(this, {
target: null,
callback: (entries) => {
if (entries[0]?.target) {
this._canScrollDown(entries[0].target);
}
},
});
protected firstUpdated(_changedProperties: PropertyValues): void {
this._contentSize.observe(this._contentElement);
}
protected render() {
@@ -53,7 +70,9 @@ export default class HaAutomationSidebarCard extends ScrollableFadeMixin(
yaml: this.yamlMode,
})}
>
<ha-dialog-header>
<ha-dialog-header
class=${classMap({ scrolled: this._contentScrolled })}
>
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
@@ -88,14 +107,34 @@ export default class HaAutomationSidebarCard extends ScrollableFadeMixin(
>
</ha-automation-editor-warning>`
: nothing}
<div class="card-content ha-scrollbar">
<div class="card-content" @scroll=${this._onScroll}>
<slot></slot>
${this.renderScrollableFades(this.isWide)}
</div>
<div
class=${classMap({ fade: true, scrollable: this._contentScrollable })}
></div>
</ha-card>
`;
}
@eventOptions({ passive: true })
private _onScroll(ev) {
const top = ev.target.scrollTop ?? 0;
this._contentScrolled = top > 0;
this._canScrollDown(ev.target);
}
private _canScrollDown(element: HTMLElement) {
const safeAreaInsetBottom =
parseFloat(
getComputedStyle(element).getPropertyValue("--safe-area-inset-bottom")
) || 0;
this._contentScrollable =
(element.scrollHeight ?? 0) - (element.clientHeight ?? 0) >
(element.scrollTop ?? 0) + safeAreaInsetBottom + 16;
}
private _closeSidebar() {
fireEvent(this, "close-sidebar");
}
@@ -105,63 +144,86 @@ export default class HaAutomationSidebarCard extends ScrollableFadeMixin(
ev.preventDefault();
}
static get styles() {
return [
...super.styles,
haStyleScrollbar,
css`
ha-card {
position: relative;
height: 100%;
width: 100%;
border-color: var(--primary-color);
border-width: 2px;
display: flex;
flex-direction: column;
}
static styles = css`
ha-card {
position: relative;
height: 100%;
width: 100%;
border-color: var(--primary-color);
border-width: 2px;
display: flex;
flex-direction: column;
}
@media all and (max-width: 870px) {
ha-card.mobile {
border: none;
box-shadow: none;
}
ha-card.mobile {
border-bottom-right-radius: var(--ha-border-radius-square);
border-bottom-left-radius: var(--ha-border-radius-square);
}
}
@media all and (max-width: 870px) {
ha-card.mobile {
border: none;
box-shadow: none;
}
ha-card.mobile {
border-bottom-right-radius: var(--ha-border-radius-square);
border-bottom-left-radius: var(--ha-border-radius-square);
}
}
ha-dialog-header {
border-radius: var(--ha-card-border-radius);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
position: relative;
background-color: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
}
ha-dialog-header {
border-radius: var(--ha-card-border-radius);
box-shadow: none;
transition: box-shadow 180ms ease-in-out;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
position: relative;
background-color: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
}
.card-content {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
margin-top: 0;
padding-bottom: max(var(--safe-area-inset-bottom, 0px), 32px);
}
ha-dialog-header.scrolled {
box-shadow: var(--bar-box-shadow);
}
.fade-top {
top: var(--ha-space-17);
}
.fade {
position: absolute;
bottom: 1px;
left: 1px;
right: 1px;
height: 16px;
pointer-events: none;
transition: box-shadow 180ms ease-in-out;
background-color: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
transform: rotate(180deg);
border-radius: var(--ha-card-border-radius);
border-bottom-left-radius: var(--ha-border-radius-square);
border-bottom-right-radius: var(--ha-border-radius-square);
}
@media all and (max-width: 870px) {
.card-content {
padding-bottom: 42px;
}
}
`,
];
}
.fade.scrollable {
box-shadow: var(--bar-box-shadow);
}
.card-content {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
margin-top: 0;
padding-bottom: max(var(--safe-area-inset-bottom, 0px), 32px);
}
@media all and (max-width: 870px) {
.fade {
bottom: 0;
border-radius: var(--ha-border-radius-square);
}
.card-content {
padding-bottom: 42px;
}
}
`;
}
declare global {

View File

@@ -16,16 +16,11 @@ import { classMap } from "lit/directives/class-map";
import { keyed } from "lit/directives/keyed";
import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors";
import type {
LegacyCondition,
ConditionSidebarConfig,
} from "../../../../data/automation";
import { testCondition } from "../../../../data/automation";
import {
CONDITION_BUILDING_BLOCKS,
getConditionDomain,
getConditionObjectId,
} from "../../../../data/condition";
testCondition,
type ConditionSidebarConfig,
} from "../../../../data/automation";
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
import { validateConfig } from "../../../../data/config";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
@@ -89,25 +84,14 @@ export default class HaAutomationSidebarCondition extends LitElement {
"ui.panel.config.automation.editor.conditions.condition"
);
const domain =
"condition" in this.config.config &&
getConditionDomain(this.config.config.condition);
const conditionName =
"condition" in this.config.config &&
getConditionObjectId(this.config.config.condition);
const title =
this.hass.localize(
`ui.panel.config.automation.editor.conditions.type.${type as LegacyCondition["condition"]}.label`
) ||
this.hass.localize(
`component.${domain}.conditions.${conditionName}.name`
) ||
type;
`ui.panel.config.automation.editor.conditions.type.${type}.label`
) || type;
const description = isBuildingBlock
? this.hass.localize(
`ui.panel.config.automation.editor.conditions.type.${type as LegacyCondition["condition"]}.description.picker`
`ui.panel.config.automation.editor.conditions.type.${type}.description.picker`
)
: "";
@@ -298,7 +282,6 @@ export default class HaAutomationSidebarCondition extends LitElement {
class="sidebar-editor"
.hass=${this.hass}
.condition=${this.config.config}
.description=${this.config.description}
.yamlMode=${this.yamlMode}
.uiSupported=${this.config.uiSupported}
@value-changed=${this._valueChangedSidebar}

View File

@@ -1,10 +1,14 @@
import { mdiOpenInNew } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-analytics";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-checkbox";
import "../../../components/ha-settings-row";
import "../../../components/ha-svg-icon";
import type { Analytics } from "../../../data/analytics";
import {
getAnalyticsDetails,
@@ -13,8 +17,6 @@ import {
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-alert";
@customElement("ha-config-analytics")
class ConfigAnalytics extends LitElement {
@@ -32,22 +34,10 @@ class ConfigAnalytics extends LitElement {
: undefined;
return html`
<ha-card
outlined
.header=${this.hass.localize("ui.panel.config.analytics.header") ||
"Home Assistant analytics"}
>
<ha-card outlined>
<div class="card-content">
${error ? html`<div class="error">${error}</div>` : nothing}
<p>
${this.hass.localize("ui.panel.config.analytics.intro")}
<a
href=${documentationUrl(this.hass, "/integrations/analytics/")}
target="_blank"
rel="noreferrer"
>${this.hass.localize("ui.panel.config.analytics.learn_more")}</a
>.
</p>
${error ? html`<div class="error">${error}</div>` : ""}
<p>${this.hass.localize("ui.panel.config.analytics.intro")}</p>
<ha-analytics
translation_key_panel="config"
@analytics-preferences-changed=${this._preferencesChanged}
@@ -55,59 +45,26 @@ class ConfigAnalytics extends LitElement {
.analytics=${this._analyticsDetails}
></ha-analytics>
</div>
</ha-card>
${this._analyticsDetails &&
"snapshots" in this._analyticsDetails.preferences
? html`<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.header"
<div class="card-actions">
<ha-button @click=${this._save}>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.save_button"
)}
>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.info"
)}
<a
href=${documentationUrl(this.hass, "/device-database/")}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.learn_more"
)}</a
>.
</p>
<ha-alert
.title=${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.alert.title"
)}
>${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.alert.content"
)}</ha-alert
>
<ha-settings-row>
<span slot="heading" data-for="snapshots">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.title`
)}
</span>
<span slot="description" data-for="snapshots">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.description`
)}
</span>
<ha-switch
@change=${this._handleDeviceRowClick}
.checked=${!!this._analyticsDetails?.preferences.snapshots}
.disabled=${this._analyticsDetails === undefined}
name="snapshots"
>
</ha-switch>
</ha-settings-row>
</div>
</ha-card>`
: nothing}
</ha-button>
</div>
</ha-card>
<div class="footer">
<ha-button
size="small"
appearance="plain"
href=${documentationUrl(this.hass, "/integrations/analytics/")}
target="_blank"
rel="noreferrer"
>
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
${this.hass.localize("ui.panel.config.analytics.learn_more")}
</ha-button>
</div>
`;
}
@@ -139,25 +96,11 @@ class ConfigAnalytics extends LitElement {
}
}
private _handleDeviceRowClick(ev: Event) {
const target = ev.target as HaSwitch;
this._analyticsDetails = {
...this._analyticsDetails!,
preferences: {
...this._analyticsDetails!.preferences,
snapshots: target.checked,
},
};
this._save();
}
private _preferencesChanged(event: CustomEvent): void {
this._analyticsDetails = {
...this._analyticsDetails!,
preferences: event.detail.preferences,
};
this._save();
}
static get styles(): CSSResultGroup {
@@ -174,10 +117,21 @@ class ConfigAnalytics extends LitElement {
p {
margin-top: 0;
}
ha-card:not(:first-of-type) {
margin-top: 24px;
.card-actions {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
align-items: center;
}
`,
.footer {
padding: 32px 0 16px;
text-align: center;
}
ha-button[size="small"] ha-svg-icon {
--mdc-icon-size: 16px;
}
`, // row-reverse so we tab first to "save"
];
}
}

View File

@@ -36,6 +36,26 @@ const STRATEGIES = [
description:
"ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview.description",
},
{
type: "areas",
images: {
light: "/static/images/dashboard-options/light/icon-dashboard-areas.svg",
dark: "/static/images/dashboard-options/dark/icon-dashboard-areas.svg",
},
name: "ui.panel.config.lovelace.dashboards.dialog_new.strategy.areas.title",
description:
"ui.panel.config.lovelace.dashboards.dialog_new.strategy.areas.description",
},
{
type: "home",
images: {
light: "/static/images/dashboard-options/light/icon-dashboard-home.svg",
dark: "/static/images/dashboard-options/dark/icon-dashboard-home.svg",
},
name: "ui.panel.config.lovelace.dashboards.dialog_new.strategy.home.title",
description:
"ui.panel.config.lovelace.dashboards.dialog_new.strategy.home.description",
},
{
type: "map",
images: {

View File

@@ -1,9 +1,7 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
import { stringCompare } from "../../../../common/string/compare";
import { titleCase } from "../../../../common/string/title-case";
import "../../../../components/ha-card";
import type { DeviceRegistryEntry } from "../../../../data/device_registry";
@@ -11,61 +9,16 @@ import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { createSearchParam } from "../../../../common/url/search-params";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import "../../../../components/ha-icon";
import "../../../../components/ha-label";
import type { LabelRegistryEntry } from "../../../../data/label_registry";
import { subscribeLabelRegistry } from "../../../../data/label_registry";
import { computeCssColor } from "../../../../common/color/compute-color";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
@customElement("ha-device-info-card")
export class HaDeviceCard extends SubscribeMixin(LitElement) {
export class HaDeviceCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public device!: DeviceRegistryEntry;
@property({ type: Boolean }) public narrow = false;
@state() private _labelRegistry?: LabelRegistryEntry[];
private _labelsData = memoizeOne(
(
labels: LabelRegistryEntry[] | undefined,
labelIds: string[],
language: string
): {
map: Map<string, LabelRegistryEntry>;
ids: string[];
} => {
const map = labels
? new Map(labels.map((label) => [label.label_id, label]))
: new Map<string, LabelRegistryEntry>();
const ids = [...labelIds].sort((labelA, labelB) =>
stringCompare(
map.get(labelA)?.name || labelA,
map.get(labelB)?.name || labelB,
language
)
);
return { map, ids };
}
);
public hassSubscribe() {
return [
subscribeLabelRegistry(this.hass.connection, (labels) => {
this._labelRegistry = labels;
}),
];
}
protected render(): TemplateResult {
const { map: labelMap, ids: labels } = this._labelsData(
this._labelRegistry,
this.device.labels,
this.hass.locale.language
);
return html`
<ha-card
outlined
@@ -105,7 +58,7 @@ export class HaDeviceCard extends SubscribeMixin(LitElement) {
<span class="hub"
><a
href="/config/devices/device/${this.device.via_device_id}"
>${this._computeDeviceNameDisplay(
>${this._computeDeviceNameDislay(
this.device.via_device_id
)}</a
></span
@@ -173,34 +126,6 @@ export class HaDeviceCard extends SubscribeMixin(LitElement) {
</div>
`
)}
${labels.length > 0
? html`
<div class="extra-info labels">
${labels.map((labelId) => {
const label = labelMap.get(labelId);
const color =
label?.color && typeof label.color === "string"
? computeCssColor(label.color)
: undefined;
return html`
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label?.description}
>
${label?.icon
? html`<ha-icon
slot="icon"
.icon=${label.icon}
></ha-icon>`
: nothing}
${label?.name || labelId}
</ha-label>
`;
})}
</div>
`
: nothing}
<slot></slot>
</div>
<slot name="actions"></slot>
@@ -214,7 +139,7 @@ export class HaDeviceCard extends SubscribeMixin(LitElement) {
);
}
private _computeDeviceNameDisplay(deviceId: string) {
private _computeDeviceNameDislay(deviceId) {
const device = this.hass.devices[deviceId];
return device
? computeDeviceNameDisplay(device, this.hass)
@@ -237,26 +162,8 @@ export class HaDeviceCard extends SubscribeMixin(LitElement) {
.device {
width: 30%;
}
.labels {
display: flex;
flex-wrap: wrap;
gap: var(--ha-space-1);
width: 100%;
max-width: 100%;
}
.labels ha-label {
min-width: 0;
max-width: 100%;
flex: 0 1 auto;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
--ha-label-text-color: var(--primary-text-color);
--ha-label-icon-color: var(--primary-text-color);
}
.extra-info {
margin-top: var(--ha-space-2);
margin-top: 8px;
word-wrap: break-word;
}
.manuf,

View File

@@ -1482,8 +1482,8 @@ export class HaConfigDevicePage extends LitElement {
flex-wrap: wrap;
margin: auto;
max-width: 1000px;
margin-top: var(--ha-space-8);
margin-bottom: var(--ha-space-8);
margin-top: 32px;
margin-bottom: 32px;
}
:host([narrow]) .container {
margin-top: 0;
@@ -1493,12 +1493,12 @@ export class HaConfigDevicePage extends LitElement {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: var(--ha-space-3);
padding-bottom: 12px;
}
.card-header ha-icon-button {
margin-right: calc(var(--ha-space-2) * -1);
margin-inline-end: calc(var(--ha-space-2) * -1);
margin-right: -8px;
margin-inline-end: -8px;
margin-inline-start: initial;
color: var(--primary-color);
height: auto;
@@ -1506,7 +1506,7 @@ export class HaConfigDevicePage extends LitElement {
}
.device-info {
padding: var(--ha-space-4);
padding: 16px;
}
h1 {
@@ -1528,15 +1528,15 @@ export class HaConfigDevicePage extends LitElement {
.header-name {
display: flex;
align-items: center;
padding-left: var(--ha-space-2);
padding-inline-start: var(--ha-space-2);
padding-left: 8px;
padding-inline-start: 8px;
padding-inline-end: initial;
direction: var(--direction);
}
.column,
.fullwidth {
padding: var(--ha-space-2);
padding: 8px;
box-sizing: border-box;
}
.column {
@@ -1566,8 +1566,8 @@ export class HaConfigDevicePage extends LitElement {
}
.header-right > *:not(:first-child) {
margin-left: var(--ha-space-4);
margin-inline-start: var(--ha-space-4);
margin-left: 16px;
margin-inline-start: 16px;
margin-inline-end: initial;
direction: var(--direction);
}
@@ -1580,7 +1580,7 @@ export class HaConfigDevicePage extends LitElement {
}
.column > *:not(:first-child) {
margin-top: var(--ha-space-4);
margin-top: 16px;
}
:host([narrow]) .column {
@@ -1600,7 +1600,7 @@ export class HaConfigDevicePage extends LitElement {
display: block;
width: 18px;
height: 18px;
margin-inline-start: var(--ha-space-2);
margin-inline-start: 8px;
margin-inline-end: initial;
}
@@ -1610,7 +1610,7 @@ export class HaConfigDevicePage extends LitElement {
}
.items {
padding-bottom: var(--ha-space-4);
padding-bottom: 16px;
}
ha-card:has(ha-logbook) {
@@ -1631,8 +1631,7 @@ export class HaConfigDevicePage extends LitElement {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--ha-space-1) var(--ha-space-4) var(--ha-space-1)
var(--ha-space-1);
padding: 4px 16px 4px 4px;
}
`,
];

View File

@@ -1,257 +0,0 @@
import {
mdiDelete,
mdiWater,
mdiDragHorizontalVariant,
mdiPencil,
mdiPlus,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { repeat } from "lit/directives/repeat";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card";
import "../../../../components/ha-button";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import type {
DeviceConsumptionEnergyPreference,
EnergyPreferences,
EnergyPreferencesValidation,
} from "../../../../data/energy";
import { saveEnergyPreferences } from "../../../../data/energy";
import type { StatisticsMetaData } from "../../../../data/recorder";
import { getStatisticLabel } from "../../../../data/recorder";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { documentationUrl } from "../../../../util/documentation-url";
import { showEnergySettingsDeviceWaterDialog } from "../dialogs/show-dialogs-energy";
import "./ha-energy-validation-result";
import { energyCardStyles } from "./styles";
@customElement("ha-energy-device-settings-water")
export class EnergyDeviceSettingsWater extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public preferences!: EnergyPreferences;
@property({ attribute: false })
public statsMetadata?: Record<string, StatisticsMetaData>;
@property({ attribute: false })
public validationResult?: EnergyPreferencesValidation;
protected render(): TemplateResult {
return html`
<ha-card outlined>
<h1 class="card-header">
<ha-svg-icon .path=${mdiWater}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.title"
)}
</h1>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.sub"
)}
<a
target="_blank"
rel="noopener noreferrer"
href=${documentationUrl(
this.hass,
"/docs/energy/water/#individual-devices"
)}
>${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.learn_more"
)}</a
>
</p>
${this.validationResult?.device_consumption_water.map(
(result) => html`
<ha-energy-validation-result
.hass=${this.hass}
.issues=${result}
></ha-energy-validation-result>
`
)}
<h3>
${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.devices"
)}
</h3>
<ha-sortable handle-selector=".handle" @item-moved=${this._itemMoved}>
<div class="devices">
${repeat(
this.preferences.device_consumption_water,
(device) => device.stat_consumption,
(device) => html`
<div class="row" .device=${device}>
<div class="handle">
<ha-svg-icon
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</div>
<span class="content"
>${device.name ||
getStatisticLabel(
this.hass,
device.stat_consumption,
this.statsMetadata?.[device.stat_consumption]
)}</span
>
<ha-icon-button
.label=${this.hass.localize("ui.common.edit")}
@click=${this._editDevice}
.path=${mdiPencil}
></ha-icon-button>
<ha-icon-button
.label=${this.hass.localize("ui.common.delete")}
@click=${this._deleteDevice}
.device=${device}
.path=${mdiDelete}
></ha-icon-button>
</div>
`
)}
</div>
</ha-sortable>
<div class="row">
<ha-svg-icon .path=${mdiWater}></ha-svg-icon>
<ha-button
@click=${this._addDevice}
appearance="filled"
size="small"
>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.add_device"
)}</ha-button
>
</div>
</div>
</ha-card>
`;
}
private _itemMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
const devices = this.preferences.device_consumption_water.concat();
const device = devices.splice(oldIndex, 1)[0];
devices.splice(newIndex, 0, device);
const newPrefs = {
...this.preferences,
device_consumption_water: devices,
};
fireEvent(this, "value-changed", { value: newPrefs });
this._savePreferences(newPrefs);
}
private _editDevice(ev) {
const origDevice: DeviceConsumptionEnergyPreference =
ev.currentTarget.closest(".row").device;
showEnergySettingsDeviceWaterDialog(this, {
statsMetadata: this.statsMetadata,
device: { ...origDevice },
device_consumptions: this.preferences
.device_consumption_water as DeviceConsumptionEnergyPreference[],
saveCallback: async (newDevice) => {
const newPrefs = {
...this.preferences,
device_consumption_water:
this.preferences.device_consumption_water.map((d) =>
d === origDevice ? newDevice : d
),
};
this._sanitizeParents(newPrefs);
await this._savePreferences(newPrefs);
},
});
}
private _addDevice() {
showEnergySettingsDeviceWaterDialog(this, {
statsMetadata: this.statsMetadata,
device_consumptions: this.preferences
.device_consumption_water as DeviceConsumptionEnergyPreference[],
saveCallback: async (device) => {
await this._savePreferences({
...this.preferences,
device_consumption_water:
this.preferences.device_consumption_water.concat(device),
});
},
});
}
private _sanitizeParents(prefs: EnergyPreferences) {
const statIds = prefs.device_consumption_water.map(
(d) => d.stat_consumption
);
prefs.device_consumption_water.forEach((d) => {
if (d.included_in_stat && !statIds.includes(d.included_in_stat)) {
delete d.included_in_stat;
}
});
}
private async _deleteDevice(ev) {
const deviceToDelete: DeviceConsumptionEnergyPreference =
ev.currentTarget.device;
if (
!(await showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.config.energy.delete_source"),
}))
) {
return;
}
try {
const newPrefs = {
...this.preferences,
device_consumption_water:
this.preferences.device_consumption_water.filter(
(device) => device !== deviceToDelete
),
};
this._sanitizeParents(newPrefs);
await this._savePreferences(newPrefs);
} catch (err: any) {
showAlertDialog(this, { title: `Failed to save config: ${err.message}` });
}
}
private async _savePreferences(preferences: EnergyPreferences) {
const result = await saveEnergyPreferences(this.hass, preferences);
fireEvent(this, "value-changed", { value: result });
}
static get styles(): CSSResultGroup {
return [
haStyle,
energyCardStyles,
css`
.handle {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-energy-device-settings-water": EnergyDeviceSettingsWater;
}
}

View File

@@ -1,268 +0,0 @@
import { mdiWater } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-dialog";
import "../../../../components/ha-radio";
import "../../../../components/ha-button";
import "../../../../components/ha-select";
import "../../../../components/ha-list-item";
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
import { energyStatisticHelpUrl } from "../../../../data/energy";
import { getStatisticLabel } from "../../../../data/recorder";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { EnergySettingsDeviceWaterDialogParams } from "./show-dialogs-energy";
const volumeUnitClasses = ["volume"];
@customElement("dialog-energy-device-settings-water")
export class DialogEnergyDeviceSettingsWater
extends LitElement
implements HassDialog<EnergySettingsDeviceWaterDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: EnergySettingsDeviceWaterDialogParams;
@state() private _device?: DeviceConsumptionEnergyPreference;
@state() private _volume_units?: string[];
@state() private _error?: string;
private _excludeList?: string[];
private _possibleParents: DeviceConsumptionEnergyPreference[] = [];
public async showDialog(
params: EnergySettingsDeviceWaterDialogParams
): Promise<void> {
this._params = params;
this._device = this._params.device;
this._computePossibleParents();
this._volume_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "water")
).units;
this._excludeList = this._params.device_consumptions
.map((entry) => entry.stat_consumption)
.filter((id) => id !== this._device?.stat_consumption);
}
private _computePossibleParents() {
if (!this._device || !this._params) {
this._possibleParents = [];
return;
}
const children: string[] = [];
const devices = this._params.device_consumptions;
function getChildren(stat) {
devices.forEach((d) => {
if (d.included_in_stat === stat) {
children.push(d.stat_consumption);
getChildren(d.stat_consumption);
}
});
}
getChildren(this._device.stat_consumption);
this._possibleParents = this._params.device_consumptions.filter(
(d) =>
d.stat_consumption !== this._device!.stat_consumption &&
d.stat_consumption !== this._params?.device?.stat_consumption &&
!children.includes(d.stat_consumption)
);
}
public closeDialog() {
this._params = undefined;
this._device = undefined;
this._error = undefined;
this._excludeList = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
}
protected render() {
if (!this._params) {
return nothing;
}
const pickableUnit = this._volume_units?.join(", ") || "";
return html`
<ha-dialog
open
.heading=${html`<ha-svg-icon
.path=${mdiWater}
style="--mdc-icon-size: 32px;"
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.header"
)}`}
@closed=${this.closeDialog}
>
${this._error ? html`<p class="error">${this._error}</p>` : ""}
<div>
${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.selected_stat_intro",
{ unit: pickableUnit }
)}
</div>
<ha-statistic-picker
.hass=${this.hass}
.helpMissingEntityUrl=${energyStatisticHelpUrl}
.includeUnitClass=${volumeUnitClasses}
.value=${this._device?.stat_consumption}
.label=${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.device_consumption_water"
)}
.excludeStatistics=${this._excludeList}
@value-changed=${this._statisticChanged}
dialogInitialFocus
></ha-statistic-picker>
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.display_name"
)}
type="text"
.disabled=${!this._device}
.value=${this._device?.name || ""}
.placeholder=${this._device
? getStatisticLabel(
this.hass,
this._device.stat_consumption,
this._params?.statsMetadata?.[this._device.stat_consumption]
)
: ""}
@input=${this._nameChanged}
>
</ha-textfield>
<ha-select
.label=${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.included_in_device"
)}
.value=${this._device?.included_in_stat || ""}
.helper=${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.included_in_device_helper"
)}
.disabled=${!this._device}
@selected=${this._parentSelected}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
clearable
>
${!this._possibleParents.length
? html`
<ha-list-item disabled value="-"
>${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.no_upstream_devices"
)}</ha-list-item
>
`
: this._possibleParents.map(
(stat) => html`
<ha-list-item .value=${stat.stat_consumption}
>${stat.name ||
getStatisticLabel(
this.hass,
stat.stat_consumption,
this._params?.statsMetadata?.[stat.stat_consumption]
)}</ha-list-item
>
`
)}
</ha-select>
<ha-button
appearance="plain"
@click=${this.closeDialog}
slot="primaryAction"
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
@click=${this._save}
.disabled=${!this._device}
slot="primaryAction"
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog>
`;
}
private _statisticChanged(ev: CustomEvent<{ value: string }>) {
if (!ev.detail.value) {
this._device = undefined;
return;
}
this._device = { stat_consumption: ev.detail.value };
this._computePossibleParents();
}
private _nameChanged(ev) {
const newDevice = {
...this._device!,
name: ev.target!.value,
} as DeviceConsumptionEnergyPreference;
if (!newDevice.name) {
delete newDevice.name;
}
this._device = newDevice;
}
private _parentSelected(ev) {
const newDevice = {
...this._device!,
included_in_stat: ev.target!.value,
} as DeviceConsumptionEnergyPreference;
if (!newDevice.included_in_stat) {
delete newDevice.included_in_stat;
}
this._device = newDevice;
}
private async _save() {
try {
await this._params!.saveCallback(this._device!);
this.closeDialog();
} catch (err: any) {
this._error = err.message;
}
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-statistic-picker {
width: 100%;
}
ha-select {
margin-top: 16px;
width: 100%;
}
ha-textfield {
margin-top: 16px;
width: 100%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-energy-device-settings-water": DialogEnergyDeviceSettingsWater;
}
}

View File

@@ -83,13 +83,6 @@ export interface EnergySettingsDeviceDialogParams {
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>;
}
export interface EnergySettingsDeviceWaterDialogParams {
device?: DeviceConsumptionEnergyPreference;
device_consumptions: DeviceConsumptionEnergyPreference[];
statsMetadata?: Record<string, StatisticsMetaData>;
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>;
}
export const showEnergySettingsDeviceDialog = (
element: HTMLElement,
dialogParams: EnergySettingsDeviceDialogParams
@@ -167,17 +160,6 @@ export const showEnergySettingsGridFlowToDialog = (
});
};
export const showEnergySettingsDeviceWaterDialog = (
element: HTMLElement,
dialogParams: EnergySettingsDeviceWaterDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-energy-device-settings-water",
dialogImport: () => import("./dialog-energy-device-settings-water"),
dialogParams: dialogParams,
});
};
export const showEnergySettingsGridPowerDialog = (
element: HTMLElement,
dialogParams: EnergySettingsGridPowerDialogParams

View File

@@ -22,7 +22,6 @@ import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
import "../../../components/ha-alert";
import "./components/ha-energy-device-settings";
import "./components/ha-energy-device-settings-water";
import "./components/ha-energy-grid-settings";
import "./components/ha-energy-solar-settings";
import "./components/ha-energy-battery-settings";
@@ -33,7 +32,6 @@ import { fileDownload } from "../../../util/file_download";
const INITIAL_CONFIG: EnergyPreferences = {
energy_sources: [],
device_consumption: [],
device_consumption_water: [],
};
@customElement("ha-config-energy")
@@ -144,13 +142,6 @@ class HaConfigEnergy extends LitElement {
.validationResult=${this._validationResult}
@value-changed=${this._prefsChanged}
></ha-energy-device-settings>
<ha-energy-device-settings-water
.hass=${this.hass}
.preferences=${this._preferences!}
.statsMetadata=${this._statsMetadata}
.validationResult=${this._validationResult}
@value-changed=${this._prefsChanged}
></ha-energy-device-settings-water>
</div>
</hass-subpage>
`;

View File

@@ -25,6 +25,10 @@ import { computeCssColor } from "../../../common/color/compute-color";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import {
DEFAULT_ENTITY_NAME,
type EntityNameItem,
} from "../../../common/entity/compute_entity_name_display";
import { navigate } from "../../../common/navigate";
import type {
LocalizeFunc,
@@ -122,6 +126,11 @@ import {
import { getSignedPath } from "../../../data/auth";
import { fileDownload } from "../../../util/file_download";
const HELPER_ENTITY_NAME: EntityNameItem[] = [
{ type: "area" },
...DEFAULT_ENTITY_NAME,
];
interface HelperItem {
id: string;
name: string;
@@ -505,7 +514,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
return {
id: entityState.entity_id,
name: entityState.attributes.friendly_name || "",
name: this._formatHelperName(entityState),
entity_id: entityState.entity_id,
editable:
configEntry !== undefined || entityState.attributes.editable,
@@ -584,6 +593,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
}
);
private _formatHelperName(stateObj: HassEntity): string {
const formatted =
this.hass.formatEntityName(stateObj, HELPER_ENTITY_NAME) || "";
return (
formatted || stateObj.attributes.friendly_name || stateObj.entity_id || ""
);
}
private _labelsForEntity(entityId: string): string[] {
return (
this.hass.entities[entityId]?.labels ||

View File

@@ -41,7 +41,9 @@ export class DialogLabsProgress
return html`
<ha-md-dialog
.open=${this._open}
disable-cancel-action
hideActions
scrimClickAction=""
escapeKeyAction=""
@closed=${this._handleClosed}
>
<div slot="content">

View File

@@ -12,7 +12,7 @@ import "../../../lovelace/editor/dashboard-strategy-editor/hui-dashboard-strateg
import type { LovelaceDashboardConfigureStrategyDialogParams } from "./show-dialog-lovelace-dashboard-configure-strategy";
@customElement("dialog-lovelace-dashboard-configure-strategy")
export class DialogLovelaceDashboardConfigureStrategy extends LitElement {
export class DialogLovelaceDashboardDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: LovelaceDashboardConfigureStrategyDialogParams;
@@ -97,6 +97,6 @@ export class DialogLovelaceDashboardConfigureStrategy extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"dialog-lovelace-dashboard-configure-strategy": DialogLovelaceDashboardConfigureStrategy;
"dialog-lovelace-dashboard-configure-strategy": DialogLovelaceDashboardDetail;
}
}

View File

@@ -8,13 +8,16 @@ import "../../../../components/ha-button";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import { saveFrontendSystemData } from "../../../../data/frontend";
import type {
LovelaceDashboard,
LovelaceDashboardCreateParams,
LovelaceDashboardMutableParams,
} from "../../../../data/lovelace/dashboard";
import { DEFAULT_PANEL } from "../../../../data/panel";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showConfirmationDialog } from "../../../lovelace/custom-card-helpers";
import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail";
@customElement("dialog-lovelace-dashboard-detail")
@@ -58,9 +61,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
if (!this._params || !this._data) {
return nothing;
}
const defaultPanelUrlPath =
this.hass.systemData?.default_panel || DEFAULT_PANEL;
const titleInvalid = !this._data.title || !this._data.title.trim();
const isLovelaceDashboard = this._params.urlPath === "lovelace";
return html`
<ha-dialog
@@ -85,9 +88,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
? this.hass.localize(
"ui.panel.config.lovelace.dashboards.cant_edit_yaml"
)
: isLovelaceDashboard
: this._params.urlPath === "lovelace"
? this.hass.localize(
"ui.panel.config.lovelace.dashboards.cant_edit_lovelace"
"ui.panel.config.lovelace.dashboards.cant_edit_default"
)
: html`
<ha-form
@@ -116,9 +119,24 @@ export class DialogLovelaceDashboardDetail extends LitElement {
)}
</ha-button>
`
: nothing}
: ""}
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this._toggleDefault}
.disabled=${this._params.urlPath === "lovelace" &&
defaultPanelUrlPath === "lovelace"}
>
${this._params.urlPath === defaultPanelUrlPath
? this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.remove_default"
)
: this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.set_default"
)}
</ha-button>
`
: nothing}
: ""}
<ha-button
slot="primaryAction"
@click=${this._updateDashboard}
@@ -236,6 +254,40 @@ export class DialogLovelaceDashboardDetail extends LitElement {
};
}
private async _toggleDefault() {
const urlPath = this._params?.urlPath;
if (!urlPath) {
return;
}
const defaultPanel = this.hass.systemData?.default_panel || DEFAULT_PANEL;
// Add warning dialog to saying that this will change the default dashboard for all users
const confirm = await showConfirmationDialog(this, {
title: this.hass.localize(
urlPath === defaultPanel
? "ui.panel.config.lovelace.dashboards.detail.remove_default_confirm_title"
: "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_title"
),
text: this.hass.localize(
urlPath === defaultPanel
? "ui.panel.config.lovelace.dashboards.detail.remove_default_confirm_text"
: "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_text"
),
confirmText: this.hass.localize("ui.common.ok"),
dismissText: this.hass.localize("ui.common.cancel"),
destructive: false,
});
if (!confirm) {
return;
}
saveFrontendSystemData(this.hass.connection, "core", {
...this.hass.systemData,
default_panel: urlPath === defaultPanel ? undefined : urlPath,
});
}
private async _updateDashboard() {
if (this._params?.urlPath && !this._params.dashboard?.id) {
this.closeDialog();

View File

@@ -1,9 +1,8 @@
import {
mdiCheck,
mdiCheckCircleOutline,
mdiDelete,
mdiDotsVertical,
mdiHomeCircleOutline,
mdiHomeEdit,
mdiPencil,
mdiPlus,
} from "@mdi/js";
@@ -11,6 +10,7 @@ import type { PropertyValues } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoize from "memoize-one";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { storage } from "../../../../common/decorators/storage";
import { navigate } from "../../../../common/navigate";
import { stringCompare } from "../../../../common/string/compare";
@@ -29,7 +29,6 @@ import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import { saveFrontendSystemData } from "../../../../data/frontend";
import type { LovelacePanelConfig } from "../../../../data/lovelace";
import type { LovelaceRawConfig } from "../../../../data/lovelace/config/types";
import {
@@ -46,11 +45,7 @@ import {
fetchDashboards,
updateDashboard,
} from "../../../../data/lovelace/dashboard";
import {
DEFAULT_PANEL,
getPanelIcon,
getPanelTitle,
} from "../../../../data/panel";
import { DEFAULT_PANEL } from "../../../../data/panel";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import "../../../../layouts/hass-loading-screen";
import "../../../../layouts/hass-tabs-subpage-data-table";
@@ -61,21 +56,12 @@ import { lovelaceTabs } from "../ha-config-lovelace";
import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy";
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
export const PANEL_DASHBOARDS = [
"home",
"light",
"security",
"climate",
"energy",
] as string[];
type DataTableItem = Pick<
LovelaceDashboard,
"icon" | "title" | "show_in_sidebar" | "require_admin" | "mode" | "url_path"
> & {
default: boolean;
filename: string;
localized_type: string;
type: string;
};
@@ -126,7 +112,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
state: false,
subscribe: false,
})
private _activeGrouping?: string = "localized_type";
private _activeGrouping?: string = "type";
@storage({
key: "lovelace-dashboards-table-collapsed",
@@ -181,7 +167,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
<ha-svg-icon
.id="default-icon-${dashboard.title}"
style="padding-left: 10px; padding-inline-start: 10px; padding-inline-end: initial; direction: var(--direction);"
.path=${mdiHomeCircleOutline}
.path=${mdiCheckCircleOutline}
></ha-svg-icon>
<ha-tooltip
.for="default-icon-${dashboard.title}"
@@ -197,7 +183,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
},
};
columns.localized_type = {
columns.type = {
title: localize(
"ui.panel.config.lovelace.dashboards.picker.headers.type"
),
@@ -267,15 +253,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
.hass=${this.hass}
narrow
.items=${[
{
path: mdiHomeEdit,
label: localize(
"ui.panel.config.lovelace.dashboards.picker.set_as_default"
),
action: () => this._handleSetAsDefault(dashboard),
disabled: dashboard.default,
},
...(dashboard.type === "user_created"
...(this._canEdit(dashboard.url_path)
? [
{
path: mdiPencil,
@@ -284,6 +262,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
),
action: () => this._handleEdit(dashboard),
},
]
: []),
...(this._canDelete(dashboard.url_path)
? [
{
label: this.hass.localize(
"ui.panel.config.lovelace.dashboards.picker.delete"
@@ -306,43 +288,92 @@ export class HaConfigLovelaceDashboards extends LitElement {
private _getItems = memoize(
(dashboards: LovelaceDashboard[], defaultUrlPath: string | null) => {
const mode = (this.hass.panels?.lovelace?.config as LovelacePanelConfig)
.mode;
const defaultMode = (
this.hass.panels?.lovelace?.config as LovelacePanelConfig
).mode;
const isDefault = defaultUrlPath === "lovelace";
const result: DataTableItem[] = [
{
icon: "mdi:view-dashboard",
title: this.hass.localize("panel.states"),
default: isDefault,
show_in_sidebar: true,
show_in_sidebar: isDefault,
require_admin: false,
url_path: "lovelace",
mode: mode,
filename: mode === "yaml" ? "ui-lovelace.yaml" : "",
type: "built_in",
localized_type: this._localizeType("built_in"),
mode: defaultMode,
filename: defaultMode === "yaml" ? "ui-lovelace.yaml" : "",
type: this._localizeType("built_in"),
},
];
PANEL_DASHBOARDS.forEach((panel) => {
const panelInfo = this.hass.panels[panel];
if (!panel) {
return;
}
const item: DataTableItem = {
icon: getPanelIcon(panelInfo),
title: getPanelTitle(this.hass, panelInfo) || panelInfo.url_path,
if (isComponentLoaded(this.hass, "energy")) {
result.push({
icon: "mdi:lightning-bolt",
title: this.hass.localize(`ui.panel.config.dashboard.energy.main`),
show_in_sidebar: true,
mode: "storage",
url_path: panelInfo.url_path,
url_path: "energy",
filename: "",
default: defaultUrlPath === panelInfo.url_path,
default: false,
require_admin: false,
type: "built_in",
localized_type: this._localizeType("built_in"),
};
result.push(item);
});
type: this._localizeType("built_in"),
});
}
if (this.hass.panels.light) {
result.push({
icon: this.hass.panels.light.icon || "mdi:lamps",
title: this.hass.localize("panel.light"),
show_in_sidebar: true,
mode: "storage",
url_path: "light",
filename: "",
default: false,
require_admin: false,
type: this._localizeType("built_in"),
});
}
if (this.hass.panels.security) {
result.push({
icon: this.hass.panels.security.icon || "mdi:security",
title: this.hass.localize("panel.security"),
show_in_sidebar: true,
mode: "storage",
url_path: "security",
filename: "",
default: false,
require_admin: false,
type: this._localizeType("built_in"),
});
}
if (this.hass.panels.climate) {
result.push({
icon: this.hass.panels.climate.icon || "mdi:home-thermometer",
title: this.hass.localize("panel.climate"),
show_in_sidebar: true,
mode: "storage",
url_path: "climate",
filename: "",
default: false,
require_admin: false,
type: this._localizeType("built_in"),
});
}
if (this.hass.panels.home) {
result.push({
icon: this.hass.panels.home.icon || "mdi:home",
title: this.hass.localize("panel.home"),
show_in_sidebar: true,
mode: "storage",
url_path: "home",
filename: "",
default: false,
require_admin: false,
type: this._localizeType("built_in"),
});
}
result.push(
...dashboards
@@ -355,8 +386,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
filename: "",
...dashboard,
default: defaultUrlPath === dashboard.url_path,
type: "user_created",
localized_type: this._localizeType("user_created"),
type: this._localizeType("user_created"),
}) satisfies DataTableItem
)
);
@@ -456,32 +486,20 @@ export class HaConfigLovelaceDashboards extends LitElement {
this._openDetailDialog(dashboard, urlPath);
}
private _handleSetAsDefault = async (item: DataTableItem) => {
if (item.default) {
return;
}
private _canDelete(urlPath: string) {
return ![
"lovelace",
"energy",
"light",
"security",
"climate",
"home",
].includes(urlPath);
}
const confirm = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.set_default_confirm_title"
),
text: this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.set_default_confirm_text"
),
confirmText: this.hass.localize("ui.common.ok"),
dismissText: this.hass.localize("ui.common.cancel"),
destructive: false,
});
if (!confirm) {
return;
}
await saveFrontendSystemData(this.hass.connection, "core", {
...this.hass.systemData,
default_panel: item.url_path,
});
};
private _canEdit(urlPath: string) {
return !["light", "security", "climate", "home"].includes(urlPath);
}
private _handleDelete = async (item: DataTableItem) => {
const dashboard = this._dashboards.find(
@@ -563,6 +581,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
private async _deleteDashboard(
dashboard: LovelaceDashboard
): Promise<boolean> {
if (!this._canDelete(dashboard.url_path)) {
return false;
}
const confirm = await showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.lovelace.dashboards.confirm_delete_title",

View File

@@ -271,6 +271,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
},
area: {
title: localize("ui.panel.config.scene.picker.headers.area"),
defaultHidden: true,
groupable: true,
filterable: true,
sortable: true,

View File

@@ -281,6 +281,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
},
area: {
title: localize("ui.panel.config.script.picker.headers.area"),
defaultHidden: true,
groupable: true,
filterable: true,
sortable: true,

View File

@@ -1,7 +1,6 @@
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type { AssistPipeline } from "../../../../data/assist_pipeline";
@@ -79,18 +78,6 @@ export class AssistPipelineDetailSTT extends LitElement {
private _supportedLanguagesChanged(ev) {
this._supportedLanguages = ev.detail.value;
if (
!this.data?.stt_language ||
!this._supportedLanguages?.includes(this.data.stt_language)
) {
// wait for update of conversation_engine
setTimeout(() => {
const value = { ...this.data };
value.stt_language = this._supportedLanguages?.[0] ?? null;
fireEvent(this, "value-changed", { value });
}, 0);
}
}
static styles = css`

View File

@@ -1,7 +1,6 @@
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";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import "../../../../components/ha-button";
import "../../../../components/ha-form/ha-form";
@@ -123,18 +122,6 @@ export class AssistPipelineDetailTTS extends LitElement {
private _supportedLanguagesChanged(ev) {
this._supportedLanguages = ev.detail.value;
if (
!this.data?.tts_language ||
!this._supportedLanguages?.includes(this.data?.tts_language)
) {
// wait for update of conversation_engine
setTimeout(() => {
const value = { ...this.data };
value.tts_language = this._supportedLanguages?.[0] ?? null;
fireEvent(this, "value-changed", { value });
}, 0);
}
}
static styles = css`

View File

@@ -6,7 +6,6 @@ import {
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { formatDateTimeWithSeconds } from "../../../../common/datetime/format_date_time";
import type {
PipelineRunEvent,
@@ -21,8 +20,6 @@ import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../types";
import "./assist-render-pipeline-events";
import type { ChatLog } from "../../../../data/chat_log";
import { subscribeChatLog } from "../../../../data/chat_log";
@customElement("assist-pipeline-debug")
export class AssistPipelineDebug extends LitElement {
@@ -40,12 +37,8 @@ export class AssistPipelineDebug extends LitElement {
@state() private _events?: PipelineRunEvent[];
@state() private _chatLog?: ChatLog;
private _unsubRefreshEventsID?: number;
private _unsubChatLogUpdates?: Promise<UnsubscribeFunc>;
protected render() {
return html`<hass-subpage
.narrow=${this.narrow}
@@ -113,7 +106,6 @@ export class AssistPipelineDebug extends LitElement {
? html`<assist-render-pipeline-events
.hass=${this.hass}
.events=${this._events}
.chatLog=${this._chatLog}
></assist-render-pipeline-events>`
: ""}
</div>
@@ -128,10 +120,6 @@ export class AssistPipelineDebug extends LitElement {
clearRefresh = true;
}
if (changedProperties.has("_runId")) {
if (this._unsubChatLogUpdates) {
this._unsubChatLogUpdates.then((unsub) => unsub());
this._unsubChatLogUpdates = undefined;
}
this._fetchEvents();
clearRefresh = true;
}
@@ -147,10 +135,6 @@ export class AssistPipelineDebug extends LitElement {
clearTimeout(this._unsubRefreshEventsID);
this._unsubRefreshEventsID = undefined;
}
if (this._unsubChatLogUpdates) {
this._unsubChatLogUpdates.then((unsub) => unsub());
this._unsubChatLogUpdates = undefined;
}
}
private async _fetchRuns() {
@@ -201,27 +185,8 @@ export class AssistPipelineDebug extends LitElement {
});
return;
}
if (!this._events!.length) {
return;
}
if (!this._unsubChatLogUpdates && this._events[0].type === "run-start") {
this._unsubChatLogUpdates = subscribeChatLog(
this.hass,
this._events[0].data.conversation_id,
(chatLog) => {
if (chatLog) {
this._chatLog = chatLog;
} else {
this._unsubChatLogUpdates?.then((unsub) => unsub());
this._unsubChatLogUpdates = undefined;
}
}
);
this._unsubChatLogUpdates.catch(() => {
this._unsubChatLogUpdates = undefined;
});
}
if (
this._events?.length &&
// If the last event is not a finish run event, the run is still ongoing.
// Refresh events automatically.
!["run-end", "error"].includes(this._events[this._events.length - 1].type)

View File

@@ -1,7 +1,6 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { extractSearchParam } from "../../../../common/url/search-params";
import "../../../../components/ha-assist-pipeline-picker";
import "../../../../components/ha-button";
@@ -25,8 +24,6 @@ import type { HomeAssistant } from "../../../../types";
import { AudioRecorder } from "../../../../util/audio-recorder";
import { fileDownload } from "../../../../util/file_download";
import "./assist-render-pipeline-run";
import type { ChatLog } from "../../../../data/chat_log";
import { subscribeChatLog } from "../../../../data/chat_log";
@customElement("assist-pipeline-run-debug")
export class AssistPipelineRunDebug extends LitElement {
@@ -49,13 +46,6 @@ export class AssistPipelineRunDebug extends LitElement {
@state() private _pipelineId?: string =
extractSearchParam("pipeline") || undefined;
@state() private _chatLog?: ChatLog;
private _chatLogSubscription: {
conversationId: string;
unsub: Promise<UnsubscribeFunc>;
} | null = null;
protected render(): TemplateResult {
return html`
<hass-subpage
@@ -188,7 +178,6 @@ export class AssistPipelineRunDebug extends LitElement {
<assist-render-pipeline-run
.hass=${this.hass}
.pipelineRun=${run}
.chatLog=${this._chatLog}
></assist-render-pipeline-run>
`
)}
@@ -197,14 +186,6 @@ export class AssistPipelineRunDebug extends LitElement {
`;
}
public disconnectedCallback(): void {
super.disconnectedCallback();
if (this._chatLogSubscription) {
this._chatLogSubscription.unsub.then((unsub) => unsub());
this._chatLogSubscription = null;
}
}
private get conversationId(): string | null {
return this._pipelineRuns.length === 0
? null
@@ -427,32 +408,6 @@ export class AssistPipelineRunDebug extends LitElement {
added = true;
}
callback(updatedRun);
const conversationId = this.conversationId;
if (
!this._chatLog &&
conversationId &&
(!this._chatLogSubscription ||
this._chatLogSubscription.conversationId !== conversationId)
) {
if (this._chatLogSubscription) {
this._chatLogSubscription.unsub.then((unsub) => unsub());
}
this._chatLogSubscription = {
conversationId,
unsub: subscribeChatLog(this.hass, conversationId, (chatLog) => {
if (chatLog) {
this._chatLog = chatLog;
} else {
this._chatLogSubscription?.unsub.then((unsub) => unsub());
this._chatLogSubscription = null;
}
}),
};
this._chatLogSubscription.unsub.catch(() => {
this._chatLogSubscription = null;
});
}
},
{
...options,

View File

@@ -9,7 +9,6 @@ import type {
import { processEvent } from "../../../../data/assist_pipeline";
import type { HomeAssistant } from "../../../../types";
import "./assist-render-pipeline-run";
import type { ChatLog } from "../../../../data/chat_log";
@customElement("assist-render-pipeline-events")
export class AssistPipelineEvents extends LitElement {
@@ -17,8 +16,6 @@ export class AssistPipelineEvents extends LitElement {
@property({ attribute: false }) public events!: PipelineRunEvent[];
@property({ attribute: false }) public chatLog?: ChatLog;
private _processEvents = memoizeOne(
(events: PipelineRunEvent[]): PipelineRun | undefined => {
let run: PipelineRun | undefined;
@@ -59,7 +56,6 @@ export class AssistPipelineEvents extends LitElement {
<assist-render-pipeline-run
.hass=${this.hass}
.pipelineRun=${run}
.chatLog=${this.chatLog}
></assist-render-pipeline-run>
`;
}

View File

@@ -1,5 +1,5 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-card";
import "../../../../components/ha-alert";
@@ -12,12 +12,6 @@ import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/ha-yaml-editor";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import type {
ChatLogAssistantContent,
ChatLog,
ChatLogContent,
ChatLogUserContent,
} from "../../../../data/chat_log";
const RUN_DATA = ["pipeline", "language"];
const WAKE_WORD_DATA = ["engine"];
@@ -125,7 +119,7 @@ const dataMinusKeysRender = (
result[key] = data[key];
}
return render
? html`<ha-expansion-panel class="yaml-expansion">
? html`<ha-expansion-panel>
<span slot="header"
>${hass.localize("ui.panel.config.voice_assistants.debug.raw")}</span
>
@@ -140,8 +134,6 @@ export class AssistPipelineDebug extends LitElement {
@property({ attribute: false }) public pipelineRun!: PipelineRun;
@property({ attribute: false }) public chatLog?: ChatLog;
private _audioElement?: HTMLAudioElement;
private get _isPlaying(): boolean {
@@ -155,47 +147,31 @@ export class AssistPipelineDebug extends LitElement {
) || "ready"
: "ready";
let messages: ChatLogContent[];
const messages: { from: string; text: string }[] = [];
if (this.chatLog) {
messages = this.chatLog.content.filter(
this.pipelineRun.finished
? (content: ChatLogContent) =>
content.role === "system" ||
(content.created >= this.pipelineRun.started &&
content.created <= this.pipelineRun.finished!)
: (content: ChatLogContent) =>
content.role === "system" ||
content.created >= this.pipelineRun.started
);
} else {
messages = [];
const userMessage =
(this.pipelineRun.init_options &&
"text" in this.pipelineRun.init_options.input
? this.pipelineRun.init_options.input.text
: undefined) ||
this.pipelineRun?.stt?.stt_output?.text ||
this.pipelineRun?.intent?.intent_input;
// We don't have the chat log everywhere yet, just fallback for now.
const userMessage =
(this.pipelineRun.init_options &&
"text" in this.pipelineRun.init_options.input
? this.pipelineRun.init_options.input.text
: undefined) ||
this.pipelineRun?.stt?.stt_output?.text ||
this.pipelineRun?.intent?.intent_input;
if (userMessage) {
messages.push({
from: "user",
text: userMessage,
});
}
if (userMessage) {
messages.push({
role: "user",
content: userMessage,
} as ChatLogUserContent);
}
if (
this.pipelineRun?.intent?.intent_output?.response?.speech?.plain?.speech
) {
messages.push({
role: "assistant",
content:
this.pipelineRun.intent.intent_output.response.speech.plain.speech,
} as ChatLogAssistantContent);
}
if (
this.pipelineRun?.intent?.intent_output?.response?.speech?.plain?.speech
) {
messages.push({
from: "hass",
text: this.pipelineRun.intent.intent_output.response.speech.plain
.speech,
});
}
return html`
@@ -214,58 +190,10 @@ export class AssistPipelineDebug extends LitElement {
${messages.length > 0
? html`
<div class="messages">
${messages.map((content) =>
content.role === "system" || content.role === "tool_result"
? html`
<ha-expansion-panel
class="content-expansion ${content.role}"
>
<div slot="header">
${content.role === "system"
? "System"
: `Result for ${content.tool_name}`}
</div>
${content.role === "system"
? html`<pre>${content.content}</pre>`
: html`
<ha-yaml-editor
read-only
auto-update
.value=${content}
></ha-yaml-editor>
`}
</ha-expansion-panel>
`
: html`
${content.content
? html`
<div class=${`message ${content.role}`}>
${content.content}
</div>
`
: nothing}
${content.role === "assistant" &&
content.tool_calls?.length
? html`
<ha-expansion-panel
class="content-expansion assistant"
>
<span slot="header">
Call
${content.tool_calls.length === 1
? content.tool_calls[0].tool_name
: `${content.tool_calls.length} tools`}
</span>
<ha-yaml-editor
read-only
auto-update
.value=${content.tool_calls}
></ha-yaml-editor>
</ha-expansion-panel>
`
: nothing}
`
${messages.map(
({ from, text }) => html`
<div class=${`message ${from}`}>${text}</div>
`
)}
</div>
<div style="clear:both"></div>
@@ -514,7 +442,7 @@ export class AssistPipelineDebug extends LitElement {
: ""}
${maybeRenderError(this.pipelineRun, "tts", lastRunStage)}
<ha-card>
<ha-expansion-panel class="yaml-expansion">
<ha-expansion-panel>
<span slot="header"
>${this.hass.localize(
"ui.panel.config.voice_assistants.debug.raw"
@@ -591,12 +519,12 @@ export class AssistPipelineDebug extends LitElement {
.row > div:last-child {
text-align: right;
}
.yaml-expansion {
ha-expansion-panel {
padding-left: 8px;
padding-inline-start: 8px;
padding-inline-end: initial;
}
.card-content .yaml-expansion {
.card-content ha-expansion-panel {
padding-left: 0px;
padding-inline-start: 0px;
padding-inline-end: initial;
@@ -612,59 +540,27 @@ export class AssistPipelineDebug extends LitElement {
margin-top: 8px;
}
.content-expansion {
margin: 8px 0;
border-radius: var(--ha-border-radius-xl);
clear: both;
padding: 0 8px;
--input-fill-color: none;
max-width: calc(100% - 24px);
--expansion-panel-summary-padding: 0px;
--expansion-panel-content-padding: 0px;
}
.content-expansion *[slot="header"] {
font-weight: var(--ha-font-weight-normal);
}
.system {
background-color: var(--success-color);
}
.message {
padding: 8px;
}
.message,
.content-expansion {
font-size: var(--ha-font-size-l);
margin: 8px 0;
padding: 8px;
border-radius: var(--ha-border-radius-xl);
clear: both;
}
.messages pre {
white-space: pre-wrap;
}
.user,
.tool_result {
.message.user {
margin-left: 24px;
margin-inline-start: 24px;
margin-inline-end: initial;
float: var(--float-end);
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--light-primary-color);
color: var(--text-light-primary-color, var(--primary-text-color));
direction: var(--direction);
}
.message.user,
.content-expansion div[slot="header"] {
text-align: right;
}
.assistant {
.message.hass {
margin-right: 24px;
margin-inline-end: 24px;
margin-inline-start: initial;

View File

@@ -135,11 +135,6 @@ class HaPanelDevAction extends LitElement {
? computeObjectId(this._serviceData?.action)
: undefined;
const descriptionPlaceholders =
domain && serviceName
? this.hass.services[domain][serviceName].description_placeholders
: undefined;
return html`
<div class="content">
<p>
@@ -312,14 +307,12 @@ class HaPanelDevAction extends LitElement {
<td><pre>${field.key}</pre></td>
<td>
${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${field.key}.description`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.fields.${field.key}.description`
) || field.description}
</td>
<td>
${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${field.key}.example`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.fields.${field.key}.example`
) || field.example}
</td>
</tr>`
@@ -650,11 +643,7 @@ class HaPanelDevAction extends LitElement {
} catch (_err: any) {
value =
this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${field.key}.example`,
domain && serviceName
? this.hass.services[domain][serviceName]
.description_placeholders
: undefined
`component.${domain}.services.${serviceName}.fields.${field.key}.example`
) || field.example;
}
example[field.key] = value;

View File

@@ -30,7 +30,6 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
@state() private _preferences: EnergyPreferences = {
energy_sources: [],
device_consumption: [],
device_consumption_water: [],
};
public getCardSize() {

View File

@@ -1,71 +1,60 @@
import { mdiDownload, mdiPencil } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { mdiPencil, mdiDownload } from "@mdi/js";
import { customElement, property, state } from "lit/decorators";
import { goBack, navigate } from "../../common/navigate";
import "../../components/ha-alert";
import "../../components/ha-menu-button";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-list-item";
import "../../components/ha-menu-button";
import "../../components/ha-top-app-bar-fixed";
import "../../components/ha-alert";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../lovelace/components/hui-energy-period-selector";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import { goBack, navigate } from "../../common/navigate";
import type {
BatterySourceTypeEnergyPreference,
DeviceConsumptionEnergyPreference,
EnergyPreferences,
GasSourceTypeEnergyPreference,
GridSourceTypeEnergyPreference,
SolarSourceTypeEnergyPreference,
BatterySourceTypeEnergyPreference,
GasSourceTypeEnergyPreference,
WaterSourceTypeEnergyPreference,
DeviceConsumptionEnergyPreference,
EnergyCollection,
} from "../../data/energy";
import {
computeConsumptionData,
getEnergyDataCollection,
getSummedData,
} from "../../data/energy";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
import type { StatisticValue } from "../../data/recorder";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant, PanelInfo } from "../../types";
import { fileDownload } from "../../util/file_download";
import "../lovelace/components/hui-energy-period-selector";
import "../lovelace/hui-root";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import type { StatisticValue } from "../../data/recorder";
export const DEFAULT_ENERGY_COLLECTION_KEY = "energy_dashboard";
const OVERVIEW_VIEW = {
path: "overview",
strategy: {
type: "energy-overview",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
} as LovelaceViewConfig;
const ELECTRICITY_VIEW = {
path: "electricity",
back_path: "/energy",
strategy: {
type: "energy-electricity",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
} as LovelaceViewConfig;
const WATER_VIEW = {
back_path: "/energy",
path: "water",
strategy: {
type: "energy-water",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
} as LovelaceViewConfig;
const WIZARD_VIEW = {
type: "panel",
path: "setup",
cards: [{ type: "custom:energy-setup-wizard-card" }],
const ENERGY_LOVELACE_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "energy-overview",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
},
{
strategy: {
type: "energy-electricity",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
path: "electricity",
},
{
type: "panel",
path: "setup",
cards: [{ type: "custom:energy-setup-wizard-card" }],
},
],
};
@customElement("ha-panel-energy")
@@ -74,96 +63,70 @@ class PanelEnergy extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ attribute: false }) public panel?: PanelInfo;
@state() private _lovelace?: Lovelace;
@state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _error?: string;
@property({ attribute: false }) public route?: {
path: string;
prefix: string;
};
@state()
private _prefs?: EnergyPreferences;
private _energyCollection?: EnergyCollection;
@state()
private _error?: string;
get _viewPath(): string | undefined {
const viewPath: string | undefined = this.route!.path.split("/")[1];
return viewPath ? decodeURI(viewPath) : undefined;
}
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
// Initial setup
public connectedCallback() {
super.connectedCallback();
this._loadPrefs();
}
public async willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
this._loadConfig();
return;
}
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass && oldHass.localize !== this.hass.localize) {
if (oldHass?.locale !== this.hass.locale) {
this._setLovelace();
} else if (oldHass && oldHass.localize !== this.hass.localize) {
this._reloadView();
}
}
private _fetchEnergyPrefs = async (): Promise<
EnergyPreferences | undefined
> => {
const collection = getEnergyDataCollection(this.hass, {
key: DEFAULT_ENERGY_COLLECTION_KEY,
});
try {
await collection.refresh();
} catch (err: any) {
if (err.code === "not_found") {
return undefined;
private async _loadPrefs() {
if (this._viewPath === "setup") {
await import("./cards/energy-setup-wizard-card");
} else {
this._energyCollection = getEnergyDataCollection(this.hass, {
key: DEFAULT_ENERGY_COLLECTION_KEY,
});
try {
// Have to manually refresh here as we don't want to subscribe yet
await this._energyCollection.refresh();
} catch (err: any) {
if (err.code === "not_found") {
navigate("/energy/setup");
}
this._error = err.message;
return;
}
const prefs = this._energyCollection.prefs!;
if (
prefs.device_consumption.length === 0 &&
prefs.energy_sources.length === 0
) {
// No energy sources available, start from scratch
navigate("/energy/setup");
}
throw err;
}
return collection.prefs;
};
private async _loadConfig() {
try {
this._error = undefined;
const prefs = await this._fetchEnergyPrefs();
this._prefs = prefs || ({} as EnergyPreferences);
} catch (err) {
// eslint-disable-next-line no-console
console.error("Failed to load prefs:", err);
this._prefs = {} as EnergyPreferences;
this._error = (err as Error).message || "Unknown error";
}
await this._setLovelace();
// Navigate to first view if not there yet
const firstPath = this._lovelace!.config?.views?.[0]?.path;
const viewPath: string | undefined = this.route!.path.split("/")[1];
if (viewPath !== firstPath) {
navigate(`${this.route!.prefix}/${firstPath}`);
}
}
private async _setLovelace() {
const config = await this._generateLovelaceConfig();
this._lovelace = {
config: config,
rawConfig: config,
editMode: false,
urlPath: "energy",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
private _back(ev) {
@@ -172,49 +135,31 @@ class PanelEnergy extends LitElement {
}
protected render() {
if (this._error) {
return html`
<div class="centered">
<ha-alert alert-type="error">
An error occurred loading energy preferences: ${this._error}
</ha-alert>
</div>
`;
}
if (!this._prefs) {
if (!this._energyCollection?.prefs) {
// Still loading
return html`
<div class="centered">
<ha-spinner size="large"></ha-spinner>
</div>
`;
return html`<div class="centered">
<ha-spinner size="large"></ha-spinner>
</div>`;
}
if (!this._lovelace) {
return nothing;
const { prefs } = this._energyCollection;
const isSingleView = prefs.energy_sources.every((source) =>
["grid", "solar", "battery"].includes(source.type)
);
let viewPath = this._viewPath;
if (isSingleView) {
// if only electricity sources, show electricity view directly
viewPath = "electricity";
}
const viewPath: string | undefined = this.route!.path.split("/")[1];
const views = this._lovelace.config?.views || [];
const viewIndex = Math.max(
views.findIndex((view) => view.path === viewPath),
ENERGY_LOVELACE_CONFIG.views.findIndex((view) => view.path === viewPath),
0
);
const showBack = this._searchParms.has("historyBack") || viewIndex > 0;
const showBack =
this._searchParms.has("historyBack") || (!isSingleView && viewIndex > 0);
return html`
<hui-root
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.route=${this.route}
.panel=${this.panel}
@reload-energy-panel=${this._reloadConfig}
>
<div class="toolbar" slot="toolbar">
<div class="header">
<div class="toolbar">
${showBack
? html`
<ha-icon-button-arrow-prev
@@ -240,17 +185,14 @@ class PanelEnergy extends LitElement {
.collectionKey=${DEFAULT_ENERGY_COLLECTION_KEY}
>
${this.hass.user?.is_admin
? html`
<ha-list-item
slot="overflow-menu"
graphic="icon"
@request-selected=${this._navigateConfig}
>
<ha-svg-icon slot="graphic" .path=${mdiPencil}>
</ha-svg-icon>
${this.hass!.localize("ui.panel.energy.configure")}
</ha-list-item>
`
? html` <ha-list-item
slot="overflow-menu"
graphic="icon"
@request-selected=${this._navigateConfig}
>
<ha-svg-icon slot="graphic" .path=${mdiPencil}> </ha-svg-icon>
${this.hass!.localize("ui.panel.energy.configure")}
</ha-list-item>`
: nothing}
<ha-list-item
slot="overflow-menu"
@@ -262,40 +204,45 @@ class PanelEnergy extends LitElement {
</ha-list-item>
</hui-energy-period-selector>
</div>
</hui-root>
</div>
<hui-view-container
.hass=${this.hass}
@reload-energy-panel=${this._reloadView}
>
${this._error
? html`<div class="centered">
<ha-alert alert-type="error">
An error occurred while fetching your energy preferences:
${this._error}
</ha-alert>
</div>`
: this._lovelace
? html`<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${viewIndex}
></hui-view>`
: nothing}
</hui-view-container>
`;
}
private async _generateLovelaceConfig(): Promise<LovelaceConfig> {
if (
!this._prefs ||
(this._prefs.device_consumption.length === 0 &&
this._prefs.energy_sources.length === 0)
) {
await import("./cards/energy-setup-wizard-card");
return {
views: [WIZARD_VIEW],
};
}
const isElectricityOnly = this._prefs.energy_sources.every((source) =>
["grid", "solar", "battery"].includes(source.type)
);
if (isElectricityOnly) {
return {
views: [ELECTRICITY_VIEW],
};
}
const hasWater =
this._prefs.energy_sources.some((source) => source.type === "water") ||
this._prefs.device_consumption_water?.length > 0;
const views: LovelaceViewConfig[] = [OVERVIEW_VIEW, ELECTRICITY_VIEW];
if (hasWater) {
views.push(WATER_VIEW);
}
return { views };
private _setLovelace() {
this._lovelace = {
config: ENERGY_LOVELACE_CONFIG,
rawConfig: ENERGY_LOVELACE_CONFIG,
editMode: false,
urlPath: "energy",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
private _navigateConfig(ev) {
@@ -305,9 +252,7 @@ class PanelEnergy extends LitElement {
private async _dumpCSV(ev) {
ev.stopPropagation();
const energyData = getEnergyDataCollection(this.hass, {
key: "energy_dashboard",
});
const energyData = this._energyCollection!;
if (!energyData.prefs || !energyData.state.stats) {
return;
@@ -603,8 +548,13 @@ class PanelEnergy extends LitElement {
fileDownload(url, "energy.csv");
}
private _reloadConfig() {
this._loadConfig();
private _reloadView() {
// Force strategy to be re-run by making a copy of the view
const config = this._lovelace!.config;
this._lovelace = {
...this._lovelace!,
config: { ...config, views: config.views.map((view) => ({ ...view })) },
};
}
static get styles(): CSSResultGroup {
@@ -630,6 +580,45 @@ class PanelEnergy extends LitElement {
-webkit-user-select: none;
-moz-user-select: none;
}
.header {
background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
position: fixed;
top: 0;
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-right,
0px
)
);
padding-top: var(--safe-area-inset-top);
z-index: 4;
transition: box-shadow 200ms linear;
display: flex;
flex-direction: row;
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
backdrop-filter: var(--app-header-backdrop-filter, none);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
}
:host([narrow]) .header {
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-left,
0px
) - var(--safe-area-inset-right, 0px)
);
padding-left: var(--safe-area-inset-left);
}
:host([scrolled]) .header {
box-shadow: var(
--mdc-top-app-bar-fixed-box-shadow,
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
);
}
.toolbar {
height: var(--header-height);
display: flex;
@@ -648,6 +637,24 @@ class PanelEnergy extends LitElement {
line-height: var(--ha-line-height-normal);
flex-grow: 1;
}
hui-view-container {
position: relative;
display: flex;
min-height: 100vh;
box-sizing: border-box;
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
padding-right: var(--safe-area-inset-right);
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
}
:host([narrow]) hui-view-container {
padding-left: var(--safe-area-inset-left);
padding-inline-start: var(--safe-area-inset-left);
}
hui-view {
flex: 1 1 100%;
max-width: 100%;
}
.centered {
width: 100%;
height: 100%;

View File

@@ -9,14 +9,6 @@ import type { LovelaceSectionConfig } from "../../../data/lovelace/config/sectio
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy";
const sourceHasCost = (source: Record<string, any>): boolean =>
Boolean(
source.stat_cost ||
source.stat_compensation ||
source.entity_energy_price ||
source.number_energy_price
);
@customElement("energy-overview-view-strategy")
export class EnergyViewStrategy extends ReactiveElement {
static async generate(
@@ -72,13 +64,6 @@ export class EnergyViewStrategy extends ReactiveElement {
const hasPowerDevices = prefs.device_consumption.find(
(device) => device.stat_rate
);
const hasCost = prefs.energy_sources.some(
(source) =>
sourceHasCost(source) ||
(source.type === "grid" &&
(source.flow_from?.some(sourceHasCost) ||
source.flow_to?.some(sourceHasCost)))
);
const overviewSection: LovelaceSectionConfig = {
type: "grid",
@@ -103,7 +88,7 @@ export class EnergyViewStrategy extends ReactiveElement {
collection_key: collectionKey,
});
}
if (hasCost) {
if (hasGrid || hasSolar || hasBattery || hasGas || hasWater) {
overviewSection.cards!.push({
type: "energy-sources-table",
collection_key: collectionKey,
@@ -210,10 +195,6 @@ export class EnergyViewStrategy extends ReactiveElement {
{
type: "heading",
heading: hass.localize("ui.panel.energy.overview.water"),
tap_action: {
action: "navigate",
navigation_path: "/energy/water",
},
},
{
title: hass.localize(

View File

@@ -1,86 +0,0 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { getEnergyDataCollection } from "../../../data/energy";
import type { HomeAssistant } from "../../../types";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy";
@customElement("energy-water-view-strategy")
export class EnergyWaterViewStrategy extends ReactiveElement {
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const view: LovelaceViewConfig = { cards: [] };
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
const energyCollection = getEnergyDataCollection(hass, {
key: collectionKey,
});
const prefs = energyCollection.prefs;
// No water sources available
if (
!prefs ||
(!prefs.device_consumption_water?.length &&
!prefs.energy_sources.some((source) => source.type === "water"))
) {
return view;
}
view.type = "sidebar";
const hasWater = prefs.energy_sources.some(
(source) => source.type === "water"
);
view.cards!.push({
type: "energy-compare",
collection_key: collectionKey,
});
if (hasWater) {
view.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_water_graph_title"),
type: "energy-water-graph",
collection_key: collectionKey,
});
}
if (hasWater) {
view.cards!.push({
title: hass.localize(
"ui.panel.energy.cards.energy_sources_table_title"
),
type: "energy-sources-table",
collection_key: collectionKey,
types: ["water"],
});
}
// Only include if we have at least 1 water device in the config.
if (prefs.device_consumption_water?.length) {
const showFloorsNAreas = !prefs.device_consumption_water.some(
(d) => d.included_in_stat
);
view.cards!.push({
title: hass.localize("ui.panel.energy.cards.water_sankey_title"),
type: "water-sankey",
collection_key: collectionKey,
group_by_floor: showFloorsNAreas,
group_by_area: showFloorsNAreas,
});
}
return view;
}
}
declare global {
interface HTMLElementTagNameMap {
"energy-water-view-strategy": EnergyWaterViewStrategy;
}
}

View File

@@ -400,9 +400,7 @@ class HaLogbookRenderer extends LitElement {
? `${domainToName(this.hass.localize, item.context_domain)}:
${
this.hass.localize(
`component.${item.context_domain}.services.${item.context_service}.name`,
this.hass.services[item.context_domain][item.context_service]
.description_placeholders
`component.${item.context_domain}.services.${item.context_service}.name`
) ||
this.hass.services[item.context_domain]?.[item.context_service]?.name ||
item.context_service

View File

@@ -14,7 +14,6 @@ import { getEnergyColor } from "./common/color";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import "./common/hui-energy-graph-chip";
import type {
EnergyData,
EnergySumData,
@@ -68,8 +67,6 @@ export class HuiEnergyUsageGraphCard
@state() private _compareEnd?: Date;
@state() private _total?: number;
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
@@ -103,19 +100,9 @@ export class HuiEnergyUsageGraphCard
return html`
<ha-card>
<div class="card-header">
<span>${this._config.title ? this._config.title : nothing}</span>
${this._total
? html`<hui-energy-graph-chip
.tooltip=${this._formatTotal(this._total)}
>
${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_usage_graph.total_usage",
{ num: formatNumber(this._total, this.hass.locale) }
)}
</hui-energy-graph-chip>`
: nothing}
</div>
${this._config.title
? html`<h1 class="card-header">${this._config.title}</h1>`
: ""}
<div
class="content ${classMap({
"has-header": !!this._config.title,
@@ -351,13 +338,6 @@ export class HuiEnergyUsageGraphCard
datasets.sort((a, b) => a.order - b.order);
fillDataGapsAndRoundCaps(datasets);
this._chartData = datasets;
this._total = this._processTotal(consumption);
}
private _processTotal(consumption: EnergyConsumptionData) {
return consumption.total.used_total > 0
? consumption.total.used_total
: undefined;
}
private _processDataSet(
@@ -535,9 +515,6 @@ export class HuiEnergyUsageGraphCard
height: 100%;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 0;
}
.content {

View File

@@ -80,10 +80,9 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
throw new Error("Entities need to be an array");
}
const computedStyles = getComputedStyle(this);
this._calendars = config!.entities.map((entity, idx) => ({
entity_id: entity,
backgroundColor: getColorByIndex(idx, computedStyles),
backgroundColor: getColorByIndex(idx),
}));
if (this._config?.entities !== config.entities) {

View File

@@ -394,8 +394,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
if (color) {
return color;
}
const computedStyles = getComputedStyle(this);
color = getColorByIndex(this._colorIndex, computedStyles);
color = getColorByIndex(this._colorIndex);
this._colorIndex++;
this._colorDict[entityId] = color;
return color;

View File

@@ -226,14 +226,6 @@ export interface EnergySankeyCardConfig extends EnergyCardBaseConfig {
group_by_area?: boolean;
}
export interface WaterSankeyCardConfig extends EnergyCardBaseConfig {
type: "water-sankey";
title?: string;
layout?: "vertical" | "horizontal" | "auto";
group_by_floor?: boolean;
group_by_area?: boolean;
}
export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig {
type: "power-sources-graph";
title?: string;

View File

@@ -1,464 +0,0 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../../components/ha-card";
import "../../../../components/ha-svg-icon";
import type { EnergyData } from "../../../../data/energy";
import { getEnergyDataCollection } from "../../../../data/energy";
import {
calculateStatisticSumGrowth,
getStatisticLabel,
} from "../../../../data/recorder";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types";
import type { LovelaceCard, LovelaceGridOptions } from "../../types";
import type { WaterSankeyCardConfig } from "../types";
import "../../../../components/chart/ha-sankey-chart";
import type { Link, Node } from "../../../../components/chart/ha-sankey-chart";
import { getGraphColorByIndex } from "../../../../common/color/colors";
import { formatNumber } from "../../../../common/number/format_number";
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
import { MobileAwareMixin } from "../../../../mixins/mobile-aware-mixin";
const DEFAULT_CONFIG: Partial<WaterSankeyCardConfig> = {
group_by_floor: true,
group_by_area: true,
};
@customElement("hui-water-sankey-card")
class HuiWaterSankeyCard
extends SubscribeMixin(MobileAwareMixin(LitElement))
implements LovelaceCard
{
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public layout?: string;
@state() private _config?: WaterSankeyCardConfig;
@state() private _data?: EnergyData;
protected hassSubscribeRequiredHostProps = ["_config"];
public setConfig(config: WaterSankeyCardConfig): void {
this._config = { ...DEFAULT_CONFIG, ...config };
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
getEnergyDataCollection(this.hass, {
key: this._config?.collection_key,
}).subscribe((data) => {
this._data = data;
}),
];
}
public getCardSize(): Promise<number> | number {
return 5;
}
getGridOptions(): LovelaceGridOptions {
return {
columns: 12,
min_columns: 6,
rows: 6,
min_rows: 2,
};
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
return (
changedProps.has("_config") ||
changedProps.has("_data") ||
changedProps.has("_isMobileSize")
);
}
protected render() {
if (!this._config) {
return nothing;
}
if (!this._data) {
return html`${this.hass.localize(
"ui.panel.lovelace.cards.energy.loading"
)}`;
}
const prefs = this._data.prefs;
const waterSources = prefs.energy_sources.filter(
(source) => source.type === "water"
);
const computedStyle = getComputedStyle(this);
const nodes: Node[] = [];
const links: Link[] = [];
// Calculate total water consumption from all devices
let totalWaterConsumption = 0;
prefs.device_consumption_water.forEach((device) => {
const value =
device.stat_consumption in this._data!.stats
? calculateStatisticSumGrowth(
this._data!.stats[device.stat_consumption]
) || 0
: 0;
totalWaterConsumption += value;
});
// Create home/consumption node
const homeNode: Node = {
id: "home",
label: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_distribution.home"
),
value: Math.max(0, totalWaterConsumption),
color: computedStyle.getPropertyValue("--primary-color").trim(),
index: 1,
};
nodes.push(homeNode);
// Add water source nodes
const waterColor = computedStyle
.getPropertyValue("--energy-water-color")
.trim();
waterSources.forEach((source) => {
if (source.type !== "water") {
return;
}
const value =
source.stat_energy_from in this._data!.stats
? calculateStatisticSumGrowth(
this._data!.stats[source.stat_energy_from]
) || 0
: 0;
if (value < 0.01) {
return;
}
nodes.push({
id: source.stat_energy_from,
label: getStatisticLabel(
this.hass,
source.stat_energy_from,
this._data!.statsMetadata[source.stat_energy_from]
),
value,
color: waterColor,
index: 0,
});
links.push({
source: source.stat_energy_from,
target: "home",
value,
});
});
let untrackedConsumption = homeNode.value;
const deviceNodes: Node[] = [];
const parentLinks: Record<string, string> = {};
prefs.device_consumption_water.forEach((device, idx) => {
const value =
device.stat_consumption in this._data!.stats
? calculateStatisticSumGrowth(
this._data!.stats[device.stat_consumption]
) || 0
: 0;
if (value < 0.01) {
return;
}
const node = {
id: device.stat_consumption,
label:
device.name ||
getStatisticLabel(
this.hass,
device.stat_consumption,
this._data!.statsMetadata[device.stat_consumption]
),
value,
color: getGraphColorByIndex(idx, computedStyle),
index: 4,
parent: device.included_in_stat,
};
if (node.parent) {
parentLinks[node.id] = node.parent;
links.push({
source: node.parent,
target: node.id,
});
} else {
untrackedConsumption -= value;
}
deviceNodes.push(node);
});
const devicesWithoutParent = deviceNodes.filter(
(node) => !parentLinks[node.id]
);
const { group_by_area, group_by_floor } = this._config;
if (group_by_area || group_by_floor) {
const { areas, floors } = this._groupByFloorAndArea(devicesWithoutParent);
Object.keys(floors)
.sort(
(a, b) =>
(this.hass.floors[b]?.level ?? -Infinity) -
(this.hass.floors[a]?.level ?? -Infinity)
)
.forEach((floorId) => {
let floorNodeId = `floor_${floorId}`;
if (floorId === "no_floor" || !group_by_floor) {
// link "no_floor" areas to home
floorNodeId = "home";
} else {
nodes.push({
id: floorNodeId,
label: this.hass.floors[floorId].name,
value: floors[floorId].value,
index: 2,
color: computedStyle.getPropertyValue("--primary-color").trim(),
});
links.push({
source: "home",
target: floorNodeId,
});
}
floors[floorId].areas.forEach((areaId) => {
let targetNodeId: string;
if (areaId === "no_area" || !group_by_area) {
// If group_by_area is false, link devices to floor or home
targetNodeId = floorNodeId;
} else {
// Create area node and link it to floor
const areaNodeId = `area_${areaId}`;
nodes.push({
id: areaNodeId,
label: this.hass.areas[areaId]!.name,
value: areas[areaId].value,
index: 3,
color: computedStyle.getPropertyValue("--primary-color").trim(),
});
links.push({
source: floorNodeId,
target: areaNodeId,
value: areas[areaId].value,
});
targetNodeId = areaNodeId;
}
// Link devices to the appropriate target (area, floor, or home)
areas[areaId].devices.forEach((device) => {
links.push({
source: targetNodeId,
target: device.id,
value: device.value,
});
});
});
});
} else {
devicesWithoutParent.forEach((deviceNode) => {
links.push({
source: "home",
target: deviceNode.id,
value: deviceNode.value,
});
});
}
const deviceSections = this._getDeviceSections(parentLinks, deviceNodes);
deviceSections.forEach((section, index) => {
section.forEach((node: Node) => {
nodes.push({ ...node, index: 4 + index });
});
});
// untracked consumption
if (untrackedConsumption > 0) {
nodes.push({
id: "untracked",
label: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_detail_graph.untracked_consumption"
),
value: untrackedConsumption,
color: computedStyle
.getPropertyValue("--state-unavailable-color")
.trim(),
index: 3 + deviceSections.length,
});
links.push({
source: "home",
target: "untracked",
value: untrackedConsumption,
});
}
const hasData = nodes.some((node) => node.value > 0);
const vertical =
this._config.layout === "vertical" ||
(this._config.layout !== "horizontal" && this._isMobileSize);
return html`
<ha-card
.header=${this._config.title}
class=${classMap({
"is-grid": this.layout === "grid",
"is-panel": this.layout === "panel",
"is-vertical": vertical,
})}
>
<div class="card-content">
${hasData
? html`<ha-sankey-chart
.data=${{ nodes, links }}
.vertical=${vertical}
.valueFormatter=${this._valueFormatter}
></ha-sankey-chart>`
: html`${this.hass.localize(
"ui.panel.lovelace.cards.energy.no_data_period"
)}`}
</div>
</ha-card>
`;
}
private _valueFormatter = (value: number) =>
`${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)} ${this._data!.waterUnit}`;
protected _groupByFloorAndArea(deviceNodes: Node[]) {
const areas: Record<string, { value: number; devices: Node[] }> = {
no_area: {
value: 0,
devices: [],
},
};
const floors: Record<string, { value: number; areas: string[] }> = {
no_floor: {
value: 0,
areas: ["no_area"],
},
};
deviceNodes.forEach((deviceNode) => {
const entity = this.hass.states[deviceNode.id];
const { area, floor } = entity
? getEntityContext(
entity,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
)
: { area: null, floor: null };
if (area) {
if (area.area_id in areas) {
areas[area.area_id].value += deviceNode.value;
areas[area.area_id].devices.push(deviceNode);
} else {
areas[area.area_id] = {
value: deviceNode.value,
devices: [deviceNode],
};
}
// see if the area has a floor
if (floor) {
if (floor.floor_id in floors) {
floors[floor.floor_id].value += deviceNode.value;
if (!floors[floor.floor_id].areas.includes(area.area_id)) {
floors[floor.floor_id].areas.push(area.area_id);
}
} else {
floors[floor.floor_id] = {
value: deviceNode.value,
areas: [area.area_id],
};
}
} else {
floors.no_floor.value += deviceNode.value;
if (!floors.no_floor.areas.includes(area.area_id)) {
floors.no_floor.areas.unshift(area.area_id);
}
}
} else {
areas.no_area.value += deviceNode.value;
areas.no_area.devices.push(deviceNode);
}
});
return { areas, floors };
}
/**
* Organizes device nodes into hierarchical sections based on parent-child relationships.
*/
protected _getDeviceSections(
parentLinks: Record<string, string>,
deviceNodes: Node[]
): Node[][] {
const parentSection: Node[] = [];
const childSection: Node[] = [];
const parentIds = Object.values(parentLinks);
const remainingLinks: typeof parentLinks = {};
deviceNodes.forEach((deviceNode) => {
const isChild = deviceNode.id in parentLinks;
const isParent = parentIds.includes(deviceNode.id);
if (isParent && !isChild) {
// Top-level parents (have children but no parents themselves)
parentSection.push(deviceNode);
} else {
childSection.push(deviceNode);
}
});
// Filter out links where parent is already in current parent section
Object.entries(parentLinks).forEach(([child, parent]) => {
if (!parentSection.some((node) => node.id === parent)) {
remainingLinks[child] = parent;
}
});
if (parentSection.length > 0) {
// Recursively process child section with remaining links
return [
parentSection,
...this._getDeviceSections(remainingLinks, childSection),
];
}
// Base case: no more parent-child relationships to process
return [deviceNodes];
}
static styles = css`
ha-card {
height: 400px;
display: flex;
flex-direction: column;
--chart-max-height: none;
}
ha-card.is-vertical {
height: 500px;
}
ha-card.is-grid,
ha-card.is-panel {
height: 100%;
}
.card-content {
flex: 1;
display: flex;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hui-water-sankey-card": HuiWaterSankeyCard;
}
}

View File

@@ -67,10 +67,7 @@ export const handleAction = async (
await hass.loadBackendTranslation("title");
const localize = await hass.loadBackendTranslation("services");
serviceName = `${domainToName(localize, domain)}: ${
localize(
`component.${domain}.services.${service}.name`,
hass.services[domain][service].description_placeholders
) ||
localize(`component.${domain}.services.${service}.name`) ||
serviceDomains[domain][service].name ||
service
}`;

View File

@@ -66,7 +66,6 @@ const LAZY_LOAD_TYPES = {
"energy-usage-graph": () =>
import("../cards/energy/hui-energy-usage-graph-card"),
"energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"),
"water-sankey": () => import("../cards/water/hui-water-sankey-card"),
"power-sources-graph": () =>
import("../cards/energy/hui-power-sources-graph-card"),
"power-sankey": () => import("../cards/energy/hui-power-sankey-card"),

View File

@@ -2,7 +2,6 @@ import type { HomeAssistant } from "../../../types";
import type { Lovelace } from "../types";
import { deleteBadge } from "./config-util";
import type { LovelaceCardPath } from "./lovelace-path";
import { fireEvent } from "../../../common/dom/fire_event";
export interface DeleteBadgeParams {
path: LovelaceCardPath;
@@ -24,13 +23,14 @@ export async function performDeleteBadge(
return;
}
const action = async () => {
lovelace.saveConfig(oldConfig);
};
lovelace.showToast({
message: hass.localize("ui.common.successfully_deleted"),
duration: 8000,
action: {
action: () => fireEvent(window, "undo-change"),
text: hass.localize("ui.common.undo"),
},
action: { action, text: hass.localize("ui.common.undo") },
});
} catch (err: any) {
// eslint-disable-next-line no-console

View File

@@ -2,7 +2,6 @@ import type { HomeAssistant } from "../../../types";
import type { Lovelace } from "../types";
import { deleteCard } from "./config-util";
import type { LovelaceCardPath } from "./lovelace-path";
import { fireEvent } from "../../../common/dom/fire_event";
export interface DeleteCardParams {
path: LovelaceCardPath;
@@ -24,13 +23,14 @@ export async function performDeleteCard(
return;
}
const action = async () => {
lovelace.saveConfig(oldConfig);
};
lovelace.showToast({
message: hass.localize("ui.common.successfully_deleted"),
duration: 8000,
action: {
action: () => fireEvent(window, "undo-change"),
text: hass.localize("ui.common.undo"),
},
action: { action, text: hass.localize("ui.common.undo") },
});
} catch (err: any) {
// eslint-disable-next-line no-console

View File

@@ -11,12 +11,10 @@ import {
mdiMagnify,
mdiPencil,
mdiPlus,
mdiRedo,
mdiRefresh,
mdiRobot,
mdiShape,
mdiSofa,
mdiUndo,
mdiViewDashboard,
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
@@ -37,7 +35,6 @@ import {
removeSearchParam,
} from "../../common/url/search-params";
import { debounce } from "../../common/util/debounce";
import { isMobileClient } from "../../util/is_mobile";
import { afterNextRender } from "../../common/util/render-status";
import "../../components/ha-button";
import "../../components/ha-button-menu";
@@ -53,10 +50,7 @@ import "../../components/ha-tab-group-tab";
import "../../components/ha-tooltip";
import { createAreaRegistryEntry } from "../../data/area_registry";
import type { LovelacePanelConfig } from "../../data/lovelace";
import type {
LovelaceConfig,
LovelaceRawConfig,
} from "../../data/lovelace/config/types";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { isStrategyDashboard } from "../../data/lovelace/config/types";
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
import {
@@ -98,7 +92,6 @@ import "./views/hui-view";
import type { HUIView } from "./views/hui-view";
import "./views/hui-view-background";
import "./views/hui-view-container";
import { UndoRedoController } from "../../common/controllers/undo-redo-controller";
interface ActionItem {
icon: string;
@@ -120,14 +113,9 @@ interface SubActionItem {
visible: boolean | undefined;
}
interface UndoStackItem {
location: string;
config: LovelaceRawConfig;
}
@customElement("hui-root")
class HUIRoot extends LitElement {
@property({ attribute: false }) public panel?: PanelInfo;
@property({ attribute: false }) public panel?: PanelInfo<LovelacePanelConfig>;
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -142,22 +130,12 @@ class HUIRoot extends LitElement {
@state() private _curView?: number | "hass-unused-entities";
private _configChangedByUndo = false;
private _viewCache?: Record<string, HUIView>;
private _viewScrollPositions: Record<string, number> = {};
private _restoreScroll = false;
private _undoRedoController = new UndoRedoController<UndoStackItem>(this, {
apply: (config) => this._applyUndoRedo(config),
currentConfig: () => ({
location: this.route!.path.split("/")[1],
config: this.lovelace!.rawConfig,
}),
});
private _debouncedConfigChanged: () => void;
private _conversation = memoizeOne((_components) =>
@@ -179,29 +157,7 @@ class HUIRoot extends LitElement {
const result: TemplateResult[] = [];
if (this._editMode) {
result.push(
html`<ha-icon-button
slot="toolbar-icon"
.path=${mdiUndo}
@click=${this._undo}
.disabled=${!this._undoRedoController.canUndo}
id="button-undo"
>
</ha-icon-button>
<ha-tooltip placement="bottom" for="button-undo">
${this.hass.localize("ui.common.undo")}
</ha-tooltip>
<ha-icon-button
slot="toolbar-icon"
.path=${mdiRedo}
@click=${this._redo}
.disabled=${!this._undoRedoController.canRedo}
id="button-redo"
>
</ha-icon-button>
<ha-tooltip placement="bottom" for="button-redo">
${this.hass.localize("ui.common.redo")}
</ha-tooltip>
<ha-button
html`<ha-button
appearance="filled"
size="small"
class="exit-edit-mode"
@@ -295,8 +251,7 @@ class HUIRoot extends LitElement {
overflowAction: this._handleShowQuickBar,
visible: !this._editMode,
overflow: this.narrow,
suffix:
this.hass.enableShortcuts && !isMobileClient ? "(E)" : undefined,
suffix: this.hass.enableShortcuts ? "(E)" : undefined,
},
{
icon: mdiCommentProcessingOutline,
@@ -306,8 +261,7 @@ class HUIRoot extends LitElement {
visible:
!this._editMode && this._conversation(this.hass.config.components),
overflow: this.narrow,
suffix:
this.hass.enableShortcuts && !isMobileClient ? "(A)" : undefined,
suffix: this.hass.enableShortcuts ? "(A)" : undefined,
},
{
icon: mdiRefresh,
@@ -543,72 +497,68 @@ class HUIRoot extends LitElement {
})}
>
<div class="header">
<slot name="toolbar">
<div class="toolbar">
${this._editMode
? html`
<div class="main-title">
${dashboardTitle ||
this.hass!.localize("ui.panel.lovelace.editor.header")}
<ha-icon-button
slot="actionItems"
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_lovelace.edit_title"
)}
.path=${mdiPencil}
class="edit-icon"
@click=${this._editDashboard}
></ha-icon-button>
</div>
<div class="action-items">${this._renderActionItems()}</div>
`
: html`
${isSubview
? html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
slot="navigationIcon"
@click=${this._goBack}
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
${isSubview
? html`
<div class="main-title">${curViewConfig.title}</div>
`
: hasTabViews
? tabs
: html`
<div class="main-title">
${views[0]?.title ?? dashboardTitle}
</div>
`}
<div class="action-items">${this._renderActionItems()}</div>
`}
</div>
<div class="toolbar">
${this._editMode
? html`
<div class="tab-bar">
${tabs}
<div class="main-title">
${dashboardTitle ||
this.hass!.localize("ui.panel.lovelace.editor.header")}
<ha-icon-button
slot="nav"
id="add-view"
@click=${this._addView}
slot="actionItems"
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.add"
"ui.panel.lovelace.editor.edit_lovelace.edit_title"
)}
.path=${mdiPlus}
.path=${mdiPencil}
class="edit-icon"
@click=${this._editDashboard}
></ha-icon-button>
</div>
<div class="action-items">${this._renderActionItems()}</div>
`
: nothing}
</slot>
: html`
${isSubview
? html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
slot="navigationIcon"
@click=${this._goBack}
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
${isSubview
? html`<div class="main-title">${curViewConfig.title}</div>`
: hasTabViews
? tabs
: html`
<div class="main-title">
${views[0]?.title ?? dashboardTitle}
</div>
`}
<div class="action-items">${this._renderActionItems()}</div>
`}
</div>
${this._editMode
? html`
<div class="tab-bar">
${tabs}
<ha-icon-button
slot="nav"
id="add-view"
@click=${this._addView}
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.add"
)}
.path=${mdiPlus}
></ha-icon-button>
</div>
`
: nothing}
</div>
<hui-view-container
class=${this._editMode ? "has-tab-bar" : ""}
@@ -695,28 +645,6 @@ class HUIRoot extends LitElement {
window.history.scrollRestoration = "auto";
}
protected willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has("lovelace")) {
const oldLovelace = changedProperties.get("lovelace") as
| Lovelace
| undefined;
if (
oldLovelace &&
this.lovelace!.rawConfig !== oldLovelace!.rawConfig &&
!this._configChangedByUndo
) {
const viewPath: string | undefined = this.route!.path.split("/")[1];
this._undoRedoController.commit({
location: viewPath,
config: oldLovelace.rawConfig,
});
} else {
this._configChangedByUndo = false;
}
}
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
@@ -1101,7 +1029,6 @@ class HUIRoot extends LitElement {
private _editModeDisable(): void {
this.lovelace!.setEditMode(false);
this._undoRedoController.reset();
}
private async _editDashboard() {
@@ -1280,36 +1207,6 @@ class HUIRoot extends LitElement {
showShortcutsDialog(this);
}
private async _applyUndoRedo(item: UndoStackItem) {
this._configChangedByUndo = true;
try {
await this.lovelace!.saveConfig(item.config);
} catch (err: any) {
this._configChangedByUndo = false;
showToast(this, {
message: this.hass.localize(
"ui.panel.lovelace.editor.undo_redo_failed_to_apply_changes",
{
error: err.message,
}
),
duration: 4000,
dismissable: true,
});
return;
}
this._navigateToView(item.location);
}
private _undo() {
this._undoRedoController.undo();
}
private _redo() {
this._undoRedoController.redo();
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@@ -42,13 +42,11 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
import("../../energy/strategies/energy-overview-view-strategy"),
"energy-electricity": () =>
import("../../energy/strategies/energy-electricity-view-strategy"),
"energy-water": () =>
import("../../energy/strategies/energy-water-view-strategy"),
map: () => import("./map/map-view-strategy"),
iframe: () => import("./iframe/iframe-view-strategy"),
area: () => import("./areas/area-view-strategy"),
"areas-overview": () => import("./areas/areas-overview-view-strategy"),
"home-overview": () => import("./home/home-overview-view-strategy"),
"home-main": () => import("./home/home-main-view-strategy"),
"home-media-players": () =>
import("./home/home-media-players-view-strategy"),
"home-area": () => import("./home/home-area-view-strategy"),

View File

@@ -10,7 +10,7 @@ import {
HOME_SUMMARIES_ICONS,
} from "./helpers/home-summaries";
import type { HomeAreaViewStrategyConfig } from "./home-area-view-strategy";
import type { HomeOverviewViewStrategyConfig } from "./home-overview-view-strategy";
import type { HomeMainViewStrategyConfig } from "./home-main-view-strategy";
export interface HomeDashboardStrategyConfig {
type: "home";
@@ -75,11 +75,11 @@ export class HomeDashboardStrategy extends ReactiveElement {
views: [
{
icon: "mdi:home",
path: "overview",
path: "home",
strategy: {
type: "home-overview",
type: "home-main",
favorite_entities: config.favorite_entities,
} satisfies HomeOverviewViewStrategyConfig,
} satisfies HomeMainViewStrategyConfig,
},
...areaViews,
mediaPlayersView,

View File

@@ -1,6 +1,5 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import {
findEntities,
@@ -24,11 +23,11 @@ import type {
WeatherForecastCardConfig,
} from "../../cards/types";
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
import type { Condition } from "../../common/validate-condition";
export interface HomeOverviewViewStrategyConfig {
type: "home-overview";
export interface HomeMainViewStrategyConfig {
type: "home-main";
favorite_entities?: string[];
}
@@ -58,10 +57,10 @@ const computeAreaCard = (
};
};
@customElement("home-overview-view-strategy")
export class HomeOverviewViewStrategy extends ReactiveElement {
@customElement("home-main-view-strategy")
export class HomeMainViewStrategy extends ReactiveElement {
static async generate(
config: HomeOverviewViewStrategyConfig,
config: HomeMainViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const areas = Object.values(hass.areas);
@@ -71,12 +70,8 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
const maxColumns = 3;
const largeScreenCondition: Condition = {
condition: "screen",
media_query: "(min-width: 871px)",
};
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
const maxColumns = 2;
const floorsSections: LovelaceSectionConfig[] = [];
for (const floorStructure of home.floors) {
@@ -131,6 +126,12 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
});
}
const favoriteSection: LovelaceSectionConfig = {
type: "grid",
column_span: maxColumns,
cards: [],
};
const favoriteEntities = (config.favorite_entities || []).filter(
(entityId) => hass.states[entityId] !== undefined
);
@@ -175,70 +176,74 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
({
type: "home-summary",
summary: "light",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/light?historyBack=1",
},
grid_options: {
columns: 12,
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard),
hasClimate &&
({
type: "home-summary",
summary: "climate",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/climate?historyBack=1",
},
grid_options: {
columns: 12,
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard),
hasSecurity &&
({
type: "home-summary",
summary: "security",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/security?historyBack=1",
},
grid_options: {
columns: 12,
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard),
hasMediaPlayers &&
({
type: "home-summary",
summary: "media_players",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "media-players",
},
grid_options: {
columns: 12,
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard),
].filter(Boolean) as LovelaceCardConfig[];
const forYouSection: LovelaceSectionConfig = {
const summarySection: LovelaceSectionConfig = {
type: "grid",
cards: [
{
type: "heading",
heading: hass.localize("ui.panel.lovelace.strategy.home.for_you"),
heading_style: "title",
visibility: [largeScreenCondition],
},
],
};
const widgetSection: LovelaceSectionConfig = {
column_span: maxColumns,
cards: [],
};
if (summaryCards.length) {
widgetSection.cards!.push(...summaryCards);
summarySection.cards!.push(
{
type: "heading",
heading: hass.localize("ui.panel.lovelace.strategy.home.summaries"),
},
...summaryCards
);
}
const weatherFilter = generateEntityFilter(hass, {
@@ -246,16 +251,28 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
entity_category: "none",
});
const widgetSection: LovelaceSectionConfig = {
type: "grid",
column_span: maxColumns,
cards: [],
};
const weatherEntity = Object.keys(hass.states)
.filter(weatherFilter)
.sort()[0];
if (weatherEntity) {
widgetSection.cards!.push({
type: "weather-forecast",
entity: weatherEntity,
forecast_type: "daily",
} as WeatherForecastCardConfig);
widgetSection.cards!.push(
{
type: "heading",
heading: "",
heading_style: "subtitle",
},
{
type: "weather-forecast",
entity: weatherEntity,
forecast_type: "daily",
} as WeatherForecastCardConfig
);
}
const energyPrefs = isComponentLoaded(hass, "energy")
@@ -282,19 +299,11 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
const sections = (
[
{
type: "grid",
cards: [
// Heading to add some spacing on large screens
{
type: "heading",
heading_style: "subtitle",
visibility: [largeScreenCondition],
},
],
},
favoriteSection.cards && favoriteSection,
commonControlsSection,
summarySection.cards && summarySection,
...floorsSections,
widgetSection.cards && widgetSection,
] satisfies (LovelaceSectionRawConfig | undefined)[]
).filter(Boolean) as LovelaceSectionRawConfig[];
@@ -310,17 +319,12 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
content: `## ${hass.localize("ui.panel.lovelace.strategy.home.welcome_user", { user: "{{ user }}" })}`,
} satisfies MarkdownCardConfig,
},
sidebar: {
sections: [forYouSection, widgetSection],
content_label: hass.localize("ui.panel.lovelace.strategy.home.home"),
sidebar_label: hass.localize("ui.panel.lovelace.strategy.home.for_you"),
},
};
}
}
declare global {
interface HTMLElementTagNameMap {
"home-overview-view-strategy": HomeOverviewViewStrategy;
"home-main-view-strategy": HomeMainViewStrategy;
}
}

View File

@@ -31,7 +31,6 @@ import {
import type { HuiSection } from "../sections/hui-section";
import type { Lovelace } from "../types";
import "./hui-view-header";
import "./hui-view-sidebar";
export const DEFAULT_MAX_COLUMNS = 4;
@@ -47,8 +46,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
@property({ attribute: false }) public isStrategy = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public sections: HuiSection[] = [];
@property({ attribute: false }) public cards: HuiCard[] = [];
@@ -61,12 +58,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
@state() _dragging = false;
@state() private _showSidebar = false;
private _contentScrollTop = 0;
private _sidebarScrollTop = 0;
private _columnsController = new ResizeController(this, {
callback: (entries) => {
const totalWidth = entries[0]?.contentRect.width;
@@ -144,31 +135,16 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
const sections = this.sections;
const totalSectionCount =
this._sectionColumnCount +
(this.lovelace?.editMode ? 1 : 0) +
(this._config?.sidebar ? 1 : 0);
this._sectionColumnCount + (this.lovelace?.editMode ? 1 : 0);
const editMode = this.lovelace.editMode;
const maxColumnCount = this._columnsController.value ?? 1;
const columnCount = Math.min(maxColumnCount, totalSectionCount);
// On mobile with sidebar, use full width for whichever view is active
const contentColumnCount =
this._config?.sidebar && !this.narrow
? Math.max(1, columnCount - 1)
: columnCount;
return html`
<div
class="wrapper ${classMap({
"top-margin": Boolean(this._config?.top_margin),
"has-sidebar": Boolean(this._config?.sidebar),
narrow: this.narrow,
})}"
style=${styleMap({
"--column-count": columnCount,
"--content-column-count": contentColumnCount,
})}
>
<hui-view-header
.hass=${this.hass}
@@ -176,54 +152,38 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
.lovelace=${this.lovelace}
.viewIndex=${this.index}
.config=${this._config?.header}
style=${styleMap({
"--max-column-count": maxColumnCount,
})}
></hui-view-header>
${this.narrow && this._config?.sidebar
? html`
<div class="mobile-tabs">
<ha-control-select
.value=${this._showSidebar ? "sidebar" : "content"}
@value-changed=${this._viewChanged}
.options=${[
{
value: "content",
label: this._config.sidebar.content_label,
},
{
value: "sidebar",
label: this._config.sidebar.sidebar_label,
},
]}
>
</ha-control-select>
</div>
`
: nothing}
<div class="container">
<ha-sortable
.disabled=${!editMode}
@item-moved=${this._sectionMoved}
group="section"
handle-selector=".handle"
draggable-selector=".section"
.rollback=${false}
<ha-sortable
.disabled=${!editMode}
@item-moved=${this._sectionMoved}
group="section"
handle-selector=".handle"
draggable-selector=".section"
.rollback=${false}
>
<div
class="container ${classMap({
dense: Boolean(this._config?.dense_section_placement),
})}"
style=${styleMap({
"--total-section-count": totalSectionCount,
"--max-column-count": maxColumnCount,
})}
>
<div
class="content ${classMap({
dense: Boolean(this._config?.dense_section_placement),
"mobile-hidden": this.narrow && this._showSidebar,
})}"
>
${repeat(
sections,
(section) => this._getSectionKey(section),
(section, idx) => {
const columnSpan = Math.min(
section.config.column_span || 1,
contentColumnCount
);
const rowSpan = section.config.row_span || 1;
${repeat(
sections,
(section) => this._getSectionKey(section),
(section, idx) => {
const columnSpan = Math.min(
section.config.column_span || 1,
maxColumnCount
);
const rowSpan = section.config.row_span || 1;
return html`
return html`
<div
class="section"
style=${styleMap({
@@ -248,89 +208,72 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
</div>
</div>
`;
}
)}
${editMode
? html`
<ha-sortable
group="card"
@item-added=${this._handleCardAdded}
draggable-selector=".card"
.rollback=${false}
>
<div class="create-section-container">
<div class="drop-helper" aria-hidden="true">
<p>
${this.hass.localize(
"ui.panel.lovelace.editor.section.drop_card_create_section"
)}
</p>
</div>
<button
class="create-section"
@click=${this._createSection}
aria-label=${this.hass.localize(
"ui.panel.lovelace.editor.section.create_section"
}
)}
${editMode
? html`
<ha-sortable
group="card"
@item-added=${this._handleCardAdded}
draggable-selector=".card"
.rollback=${false}
>
<div class="create-section-container">
<div class="drop-helper" aria-hidden="true">
<p>
${this.hass.localize(
"ui.panel.lovelace.editor.section.drop_card_create_section"
)}
.title=${this.hass.localize(
"ui.panel.lovelace.editor.section.create_section"
)}
>
<ha-ripple></ha-ripple>
<ha-svg-icon .path=${mdiViewGridPlus}></ha-svg-icon>
</button>
</p>
</div>
</ha-sortable>
`
: nothing}
</div>
</ha-sortable>
${this._config?.sidebar
? html`
<hui-view-sidebar
class=${classMap({
"mobile-hidden": this.narrow && !this._showSidebar,
})}
.hass=${this.hass}
.badges=${this.badges}
.lovelace=${this.lovelace}
.viewIndex=${this.index}
.config=${this._config.sidebar}
></hui-view-sidebar>
`
: nothing}
</div>
<div class="imported-cards-section">
${editMode && this._config?.cards?.length
? html`
<div class="section imported-cards">
<div class="imported-card-header">
<p class="title">
<ha-svg-icon .path=${mdiEyeOff}></ha-svg-icon>
${this.hass.localize(
"ui.panel.lovelace.editor.section.imported_cards_title"
<button
class="create-section"
@click=${this._createSection}
aria-label=${this.hass.localize(
"ui.panel.lovelace.editor.section.create_section"
)}
.title=${this.hass.localize(
"ui.panel.lovelace.editor.section.create_section"
)}
>
<ha-ripple></ha-ripple>
<ha-svg-icon .path=${mdiViewGridPlus}></ha-svg-icon>
</button>
</div>
</ha-sortable>
`
: nothing}
${editMode && this._config?.cards?.length
? html`
<div class="section imported-cards">
<div class="imported-card-header">
<p class="title">
<ha-svg-icon .path=${mdiEyeOff}></ha-svg-icon>
${this.hass.localize(
"ui.panel.lovelace.editor.section.imported_cards_title"
)}
</p>
<p class="subtitle">
${this.hass.localize(
"ui.panel.lovelace.editor.section.imported_cards_description"
)}
</p>
</div>
<hui-section
.lovelace=${this.lovelace}
.hass=${this.hass}
.config=${this._importedCardSectionConfig(
this._config.cards
)}
</p>
<p class="subtitle">
${this.hass.localize(
"ui.panel.lovelace.editor.section.imported_cards_description"
)}
</p>
.viewIndex=${this.index}
preview
import-only
></hui-section>
</div>
<hui-section
.lovelace=${this.lovelace}
.hass=${this.hass}
.config=${this._importedCardSectionConfig(
this._config.cards
)}
.viewIndex=${this.index}
preview
import-only
></hui-section>
</div>
`
: nothing}
</div>
`
: nothing}
</div>
</ha-sortable>
</div>
`;
}
@@ -409,34 +352,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
this.lovelace!.saveConfig(newConfig);
}
private _viewChanged(ev: CustomEvent) {
const newValue = ev.detail.value;
const shouldShowSidebar = newValue === "sidebar";
if (shouldShowSidebar !== this._showSidebar) {
this._toggleView();
}
}
private _toggleView() {
// Save current scroll position
if (this._showSidebar) {
this._sidebarScrollTop = window.scrollY;
} else {
this._contentScrollTop = window.scrollY;
}
this._showSidebar = !this._showSidebar;
// Restore scroll position after view updates
this.updateComplete.then(() => {
const scrollY = this._showSidebar
? this._sidebarScrollTop
: this._contentScrollTop;
window.scrollTo(0, scrollY);
});
}
static styles = css`
:host {
--row-height: var(--ha-view-sections-row-height, 56px);
@@ -454,19 +369,14 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
}
}
.wrapper {
.wrapper.top-margin {
display: block;
padding: var(--row-gap) var(--column-gap);
box-sizing: content-box;
margin: 0 auto;
max-width: calc(
var(--column-count) * var(--column-max-width) +
(var(--column-count) - 1) * var(--column-gap)
);
margin-top: var(--top-margin);
}
.wrapper.top-margin {
margin-top: var(--top-margin);
.container > * {
position: relative;
width: 100%;
}
.section {
@@ -480,92 +390,22 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
}
.container {
display: grid;
grid-template-columns: [content-start] repeat(
var(--content-column-count),
1fr
);
gap: var(--row-gap) var(--column-gap);
padding: var(--row-gap) 0;
}
.wrapper.has-sidebar .container {
grid-template-columns:
[content-start] repeat(var(--content-column-count), 1fr)
[sidebar-start] 1fr;
}
/* On mobile with sidebar, content and sidebar both take full width */
.wrapper.narrow.has-sidebar .container {
grid-template-columns: 1fr;
}
hui-view-sidebar {
grid-column: sidebar-start / -1;
}
.wrapper.narrow hui-view-sidebar {
grid-column: 1 / -1;
padding-bottom: calc(
var(--ha-space-4) + 56px + var(--ha-space-4) +
env(safe-area-inset-bottom)
);
}
.mobile-hidden {
display: none !important;
}
.mobile-tabs {
position: fixed;
bottom: calc(var(--ha-space-4) + env(safe-area-inset-bottom));
left: 50%;
transform: translateX(-50%);
padding: 0;
z-index: 1;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.15))
drop-shadow(0 4px 16px rgba(0, 0, 0, 0.1));
}
.mobile-tabs ha-control-select {
width: max-content;
min-width: 280px;
max-width: 90%;
--control-select-thickness: 56px;
--control-select-border-radius: var(--ha-border-radius-6xl);
--control-select-background: var(--card-background-color);
--control-select-background-opacity: 1;
--control-select-color: var(--primary-color);
--control-select-padding: 6px;
}
ha-sortable {
display: contents;
}
.content {
grid-column: content-start / sidebar-start;
grid-row: 1 / -1;
--column-count: min(var(--max-column-count), var(--total-section-count));
display: grid;
align-items: start;
justify-content: center;
grid-template-columns: repeat(var(--content-column-count), 1fr);
grid-template-columns: repeat(var(--column-count), 1fr);
grid-auto-flow: row;
gap: var(--row-gap) var(--column-gap);
}
.wrapper.narrow .content {
grid-column: 1 / -1;
}
.wrapper.narrow.has-sidebar .content {
padding-bottom: calc(
var(--ha-space-4) + 56px + var(--ha-space-4) +
env(safe-area-inset-bottom)
padding: var(--row-gap) var(--column-gap);
box-sizing: content-box;
margin: 0 auto;
max-width: calc(
var(--column-count) * var(--column-max-width) +
(var(--column-count) - 1) * var(--column-gap)
);
}
.content.dense {
.container.dense {
grid-auto-flow: row dense;
}
@@ -643,7 +483,13 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
hui-view-header {
display: block;
padding: 0 var(--column-gap);
padding-top: var(--row-gap);
margin: auto;
max-width: calc(
var(--max-column-count) * var(--column-max-width) +
(var(--max-column-count) - 1) * var(--column-gap)
);
}
.imported-cards {

View File

@@ -1,57 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import type { LovelaceViewSidebarConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import "../sections/hui-section";
import type { Lovelace } from "../types";
export const DEFAULT_VIEW_SIDEBAR_LAYOUT = "start";
@customElement("hui-view-sidebar")
export class HuiViewSidebar extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public lovelace!: Lovelace;
@property({ attribute: false }) public config?: LovelaceViewSidebarConfig;
@property({ attribute: false }) public viewIndex!: number;
render() {
if (!this.lovelace) return nothing;
// Use preview mode instead of setting lovelace to avoid the sections to be
// editable as it is not yet supported
return html`
<div class="container">
${repeat(
this.config?.sections || [],
(section) => html`
<hui-section
.config=${section}
.hass=${this.hass}
.preview=${this.lovelace.editMode}
.viewIndex=${this.viewIndex}
></hui-section>
`
)}
</div>
`;
}
static styles = css`
.container {
display: flex;
flex-direction: column;
gap: var(--row-gap, 8px);
width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hui-view-sidebar": HuiViewSidebar;
}
}

View File

@@ -1,16 +1,13 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-divider";
import "../../components/ha-list-item";
import "../../components/ha-select";
import "../../components/ha-settings-row";
import { saveFrontendUserData } from "../../data/frontend";
import type { LovelaceDashboard } from "../../data/lovelace/dashboard";
import { fetchDashboards } from "../../data/lovelace/dashboard";
import { getPanelTitle } from "../../data/panel";
import type { HomeAssistant, PanelInfo } from "../../types";
import { PANEL_DASHBOARDS } from "../config/lovelace/dashboards/ha-config-lovelace-dashboards";
import type { HomeAssistant } from "../../types";
import { saveFrontendUserData } from "../../data/frontend";
const USE_SYSTEM_VALUE = "___use_system___";
@@ -50,24 +47,12 @@ class HaPickDashboardRow extends LitElement {
<ha-list-item .value=${USE_SYSTEM_VALUE}>
${this.hass.localize("ui.panel.profile.dashboard.system")}
</ha-list-item>
<ha-divider></ha-divider>
<ha-list-item value="lovelace">
${this.hass.localize("ui.panel.profile.dashboard.lovelace")}
</ha-list-item>
${PANEL_DASHBOARDS.map((panel) => {
const panelInfo = this.hass.panels[panel] as
| PanelInfo
| undefined;
if (!panelInfo) {
return nothing;
}
return html`
<ha-list-item value=${panelInfo.url_path}>
${getPanelTitle(this.hass, panelInfo)}
</ha-list-item>
`;
})}
<ha-divider></ha-divider>
<ha-list-item value="home">
${this.hass.localize("ui.panel.profile.dashboard.home")}
</ha-list-item>
${this._dashboards.map((dashboard) => {
if (!this.hass.user!.is_admin && dashboard.require_admin) {
return "";

View File

@@ -14,8 +14,6 @@ interface PassThroughNode {
id: string;
value: number;
depth: number;
sourceId: string;
targetId: string;
}
interface GraphLink extends GraphEdge {
@@ -154,8 +152,6 @@ export function createPassThroughNode(
id: `${sourceId}-${targetId}-${depth}`,
value,
depth,
sourceId,
targetId,
};
}
@@ -241,79 +237,6 @@ export function groupNodesBySection(
return nodesPerSection;
}
export function sortNodesInSections(
nodesPerSection: Record<number, Node[]>,
depths: number[]
): Record<number, Node[]> {
const sortedSections: Record<number, Node[]> = {};
depths.forEach((depth, depthIndex) => {
const sectionNodes = nodesPerSection[depth] || [];
// Sort nodes to minimize crossings
const sortedNodes = [...sectionNodes].sort((a, b) => {
const aIsPassthrough = isPassThroughNode(a);
const bIsPassthrough = isPassThroughNode(b);
// Both are passthrough nodes - sort by source position
if (aIsPassthrough && bIsPassthrough) {
// Find positions of source nodes in previous section (use already sorted section)
if (depthIndex > 0) {
const prevDepth = depths[depthIndex - 1];
const prevSection =
sortedSections[prevDepth] || nodesPerSection[prevDepth] || [];
const aSourceIndex = prevSection.findIndex((n) => {
const nodeId = isPassThroughNode(n) ? n.id : (n as GraphNode).id;
return nodeId === a.sourceId;
});
const bSourceIndex = prevSection.findIndex((n) => {
const nodeId = isPassThroughNode(n) ? n.id : (n as GraphNode).id;
return nodeId === b.sourceId;
});
if (
aSourceIndex !== bSourceIndex &&
aSourceIndex !== -1 &&
bSourceIndex !== -1
) {
return aSourceIndex - bSourceIndex;
}
}
// Fall back to target node positions in next section (not sorted yet, use original)
if (depthIndex < depths.length - 1) {
const nextDepth = depths[depthIndex + 1];
const nextSection = nodesPerSection[nextDepth] || [];
const aTargetIndex = nextSection.findIndex((n) => {
const nodeId = isPassThroughNode(n) ? n.id : (n as GraphNode).id;
return nodeId === a.targetId;
});
const bTargetIndex = nextSection.findIndex((n) => {
const nodeId = isPassThroughNode(n) ? n.id : (n as GraphNode).id;
return nodeId === b.targetId;
});
if (
aTargetIndex !== bTargetIndex &&
aTargetIndex !== -1 &&
bTargetIndex !== -1
) {
return aTargetIndex - bTargetIndex;
}
}
}
return 0;
});
sortedSections[depth] = sortedNodes;
});
return sortedSections;
}
export function createSectionNodes(nodes: Node[]): SectionNode[] {
return nodes.map(
(node: Node): SectionNode => ({
@@ -414,11 +337,10 @@ function processNodes(
);
const nodesPerSection = groupNodesBySection(nodes, passThroughNodes);
const sortedNodesPerSection = sortNodesInSections(nodesPerSection, depths);
let globalValueToSizeRatio = 0;
const sections = depths.map((depth) => {
const sectionNodes = createSectionNodes(sortedNodesPerSection[depth] || []);
const sectionNodes = createSectionNodes(nodesPerSection[depth] || []);
const availableSpace = sectionSize - (sectionNodes.length + 1) * nodeGap;
const totalValue = sectionNodes.reduce(
(acc: number, node: SectionNode) => acc + node.value,

View File

@@ -20,7 +20,6 @@ export const colorStyles = css`
--divider-color: rgba(0, 0, 0, 0.12);
--outline-color: rgba(0, 0, 0, 0.12);
--outline-hover-color: rgba(0, 0, 0, 0.24);
--shadow-color: rgba(0, 0, 0, 0.16);
/* rgb */
--rgb-primary-color: 0, 154, 199;
@@ -92,62 +91,6 @@ export const colorStyles = css`
--black-color: #000000;
--white-color: #ffffff;
/* colors - used for graphs, calendars, maps, etc */
--color-1: #4269d0;
--color-2: #f4bd4a;
--color-3: #ff725c;
--color-4: #6cc5b0;
--color-5: #a463f2;
--color-6: #ff8ab7;
--color-7: #9c6b4e;
--color-8: #97bbf5;
--color-9: #01ab63;
--color-10: #094bad;
--color-11: #c99000;
--color-12: #d84f3e;
--color-13: #49a28f;
--color-14: #048732;
--color-15: #d96895;
--color-16: #8043ce;
--color-17: #7599d1;
--color-18: #7a4c31;
--color-19: #6989f4;
--color-20: #ffd444;
--color-21: #ff957c;
--color-22: #8fe9d3;
--color-23: #62cc71;
--color-24: #ffadda;
--color-25: #c884ff;
--color-26: #badeff;
--color-27: #bf8b6d;
--color-28: #927acc;
--color-29: #97ee3f;
--color-30: #bf3947;
--color-31: #9f5b00;
--color-32: #f48758;
--color-33: #8caed6;
--color-34: #f2b94f;
--color-35: #eff26e;
--color-36: #e43872;
--color-37: #d9b100;
--color-38: #9d7a00;
--color-39: #698cff;
--color-40: #00d27e;
--color-41: #d06800;
--color-42: #009f82;
--color-43: #c49200;
--color-44: #cbe8ff;
--color-45: #fecddf;
--color-46: #c27eb6;
--color-47: #8cd2ce;
--color-48: #c4b8d9;
--color-49: #f883b0;
--color-50: #a49100;
--color-51: #f48800;
--color-52: #27d0df;
--color-53: #a04a9b;
--color-54: #4269d0;
/* history colors */
--history-unavailable-color: transparent;
@@ -281,7 +224,7 @@ export const colorStyles = css`
--table-row-alternative-background-color: var(--secondary-background-color);
--data-table-background-color: var(--card-background-color);
--markdown-code-background-color: var(--primary-background-color);
--bar-box-shadow: 0 2px 12px var(--shadow-color);
--bar-box-shadow: 0 2px 12px rgba(0, 0, 0, 0.16);
/* https://github.com/material-components/material-web/blob/master/docs/theming.md */
--mdc-theme-primary: var(--primary-color);
@@ -364,8 +307,6 @@ export const darkColorStyles = css`
--divider-color: rgba(225, 225, 225, 0.12);
--outline-color: rgba(225, 225, 225, 0.12);
--outline-hover-color: rgba(225, 225, 225, 0.24);
--shadow-color: rgba(0, 0, 0, 0.48);
--mdc-ripple-color: #aaaaaa;
--mdc-linear-progress-buffer-color: rgba(255, 255, 255, 0.1);
@@ -409,7 +350,7 @@ export const darkColorStyles = css`
--ha-button-neutral-color: #d9dae0;
--ha-button-neutral-light-color: #6a7081;
--bar-box-shadow: 0 2px 12px var(--shadow-color);
--bar-box-shadow: 0 2px 12px rgba(0, 0, 0, 0.48);
}
`;

View File

@@ -2217,9 +2217,7 @@
"sidebar_toggle": "Sidebar toggle",
"edit_sidebar": "Edit sidebar",
"edit_subtitle": "Synced on all devices",
"migrate_to_user_data": "This will change the sidebar on all the devices you are logged in to. To create a sidebar per device, you should use a different user for that device.",
"reset_to_defaults": "Reset to defaults",
"reset_confirmation": "Are you sure you want to reset the sidebar to its default configuration? This will restore the original order and visibility of all panels."
"migrate_to_user_data": "This will change the sidebar on all the devices you are logged in to. To create a sidebar per device, you should use a different user for that device."
},
"panel": {
"home": {
@@ -3239,22 +3237,6 @@
"included_in_device_helper": "If this device is already counted by another device (such as a smart switch measured by a smart breaker), selecting the upstream device prevents duplicate energy tracking.",
"no_upstream_devices": "No eligible upstream devices"
}
},
"device_consumption_water": {
"title": "Individual water devices",
"sub": "Tracking the water usage of individual devices allows Home Assistant to break down your water usage by device.",
"learn_more": "More information on how to get started.",
"devices": "Devices",
"add_device": "Add device",
"dialog": {
"header": "Add a water device",
"display_name": "Display name",
"device_consumption_water": "Device water consumption",
"selected_stat_intro": "Select the water sensor that measures the device's water usage in either of {unit}.",
"included_in_device": "Upstream device",
"included_in_device_helper": "If this device is already counted by another device (such as a water meter measured by the main water supply), selecting the upstream device prevents duplicate water tracking.",
"no_upstream_devices": "No eligible upstream devices"
}
}
},
"helpers": {
@@ -3526,7 +3508,6 @@
"edit": "Edit",
"delete": "Delete",
"add_dashboard": "Add dashboard",
"set_as_default": "Set as default",
"type": {
"user_created": "User created",
"built_in": "Built-in"
@@ -3535,7 +3516,7 @@
"confirm_delete_title": "Delete {dashboard_title}?",
"confirm_delete_text": "This dashboard will be permanently deleted.",
"cant_edit_yaml": "Dashboards created in YAML cannot be edited from the UI. Change them in configuration.yaml.",
"cant_edit_lovelace": "The Overview dashboard title and icon cannot be changed. You can create a new dashboard to get more customization options.",
"cant_edit_default": "The default dashboard, Overview, cannot be edited from the UI. You can hide it by setting another dashboard as default.",
"detail": {
"edit_dashboard": "Edit dashboard",
"new_dashboard": "Add new dashboard",
@@ -3552,7 +3533,9 @@
"set_default": "Set as default",
"remove_default": "Remove as default",
"set_default_confirm_title": "Set as default dashboard?",
"set_default_confirm_text": "This dashboard will be shown to all users when opening Home Assistant. Each user can change this in their profile."
"set_default_confirm_text": "This will replace the current default dashboard. Users can still override their default dashboard in their profile settings.",
"remove_default_confirm_title": "Remove default dashboard?",
"remove_default_confirm_text": "The default dashboard will be changed to Overview for every user. Users can still override their default dashboard in their profile settings."
}
},
"resources": {
@@ -6797,7 +6780,6 @@
},
"analytics": {
"caption": "Analytics",
"header": "Home Assistant analytics",
"description": "Learn how to share data to improve Home Assistant",
"preferences": {
"base": {
@@ -6815,21 +6797,10 @@
"diagnostics": {
"title": "Diagnostics",
"description": "Share crash reports when unexpected errors occur."
},
"snapshots": {
"title": "Devices",
"description": "Generic information about your devices.",
"header": "Device analytics",
"info": "Anonymously share data about your devices to help build the Open Home Foundations device database. This free, open source resource helps users find useful information about smart home devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign).",
"learn_more": "Learn more about the device database and how we process your data",
"alert": {
"title": "Important",
"content": "Only enable this option if you understand that your device information will be shared."
}
}
},
"need_base_enabled": "You need to enable basic analytics for this option to be available",
"learn_more": "Learn how we process your data",
"learn_more": "How we process your data",
"intro": "Share anonymized information from your installation to help make Home Assistant better and help us convince manufacturers to add local control and privacy-focused features.",
"download_device_info": "Preview device analytics"
},
@@ -7088,9 +7059,7 @@
"unamed_device": "Unnamed device",
"others": "Others",
"scenes": "Scenes",
"automations": "Automations",
"for_you": "For you",
"home": "Home"
"automations": "Automations"
},
"common_controls": {
"not_loaded": "Usage Prediction integration is not loaded.",
@@ -7177,7 +7146,6 @@
"energy_usage_graph": {
"total_consumed": "Total consumed {num} kWh",
"total_returned": "Total returned {num} kWh",
"total_usage": "{num} kWh used",
"combined_from_grid": "Combined from grid",
"consumed_solar": "Consumed solar",
"consumed_battery": "Consumed battery"
@@ -7322,7 +7290,6 @@
"editor": {
"header": "Edit UI",
"yaml_unsupported": "The edit UI is not available when in YAML mode.",
"undo_redo_failed_to_apply_changes": "Unable to apply changes: {error}",
"menu": {
"open": "Open dashboard menu",
"raw_editor": "Raw configuration editor",
@@ -9553,7 +9520,6 @@
"energy_devices_graph_title": "Individual devices total usage",
"energy_devices_detail_graph_title": "Individual devices detail usage",
"energy_sankey_title": "Energy flow",
"water_sankey_title": "Water flow",
"energy_top_consumers_title": "Top consumers",
"power_sankey_title": "Current power flow",
"power_sources_graph_title": "Power sources"

View File

@@ -2,63 +2,34 @@ import { describe, test, expect } from "vitest";
import {
getColorByIndex,
getGraphColorByIndex,
COLORS_COUNT,
COLORS,
} from "../../../src/common/color/colors";
import { theme2hex } from "../../../src/common/color/convert-color";
describe("getColorByIndex", () => {
test("return the correct color from CSS variable", () => {
const style = {
getPropertyValue: (prop) => {
if (prop === "--color-1") return "#4269d0";
if (prop === "--color-11") return "#c99000";
return "";
},
} as CSSStyleDeclaration;
expect(getColorByIndex(0, style)).toBe(theme2hex("#4269d0"));
expect(getColorByIndex(10, style)).toBe(theme2hex("#c99000"));
test("return the correct color for a given index", () => {
expect(getColorByIndex(0)).toBe(COLORS[0]);
expect(getColorByIndex(10)).toBe(COLORS[10]);
});
test("wrap around if the index is greater than the total count", () => {
const style = {
getPropertyValue: (prop) => {
if (prop === "--color-1") return "#4269d0";
if (prop === "--color-5") return "#a463f2";
return "";
},
} as CSSStyleDeclaration;
// Index 54 should wrap to color 1
expect(getColorByIndex(COLORS_COUNT, style)).toBe(theme2hex("#4269d0"));
// Index 58 should wrap to color 5
expect(getColorByIndex(COLORS_COUNT + 4, style)).toBe(theme2hex("#a463f2"));
test("wrap around if the index is greater than the length of COLORS", () => {
expect(getColorByIndex(COLORS.length)).toBe(COLORS[0]);
expect(getColorByIndex(COLORS.length + 4)).toBe(COLORS[4]);
});
});
describe("getGraphColorByIndex", () => {
test("return color from --graph-color variable when it exists", () => {
test("return the correct theme color if it exists", () => {
const style = {
getPropertyValue: (prop) => (prop === "--graph-color-1" ? "#123456" : ""),
} as CSSStyleDeclaration;
expect(getGraphColorByIndex(0, style)).toBe(theme2hex("#123456"));
});
test("fallback to --color variable when --graph-color does not exist", () => {
test("return the default color if the theme color does not exist", () => {
const style = {
getPropertyValue: (prop) => (prop === "--color-5" ? "#abcdef" : ""),
} as CSSStyleDeclaration;
// Index 4 should try --graph-color-5, then fallback to --color-5
expect(getGraphColorByIndex(4, style)).toBe(theme2hex("#abcdef"));
});
test("prefer --graph-color over --color when both exist", () => {
const style = {
getPropertyValue: (prop) => {
if (prop === "--graph-color-1") return "#111111";
if (prop === "--color-1") return "#222222";
return "";
},
} as CSSStyleDeclaration;
// Should prefer --graph-color-1
expect(getGraphColorByIndex(0, style)).toBe(theme2hex("#111111"));
getPropertyValue: () => "",
} as unknown as CSSStyleDeclaration;
expect(getGraphColorByIndex(0, style)).toBe(theme2hex(COLORS[0]));
});
});

View File

@@ -67,8 +67,6 @@ describe("Sankey Layout Functions", () => {
id: "test",
value: 10,
depth: 1,
sourceId: "source",
targetId: "target",
};
expect(isPassThroughNode(passThroughNode)).toBe(true);
});
@@ -144,14 +142,7 @@ describe("Sankey Layout Functions", () => {
];
const passThroughNodes = [
{
id: "pt1",
depth: 1,
passThrough: true,
value: 5,
sourceId: "node1",
targetId: "node2",
},
{ id: "pt1", depth: 1, passThrough: true, value: 5 },
];
const result = groupNodesBySection(
@@ -204,8 +195,6 @@ describe("Sankey Layout Functions", () => {
passThrough: true,
value: 5,
depth: 1,
sourceId: "source",
targetId: "target",
};
const result = createSectionNodes([passThroughNode]);
@@ -327,15 +316,13 @@ describe("Sankey Layout Functions", () => {
describe("createPassThroughNode", () => {
it("should create a pass-through node", () => {
const result = createPassThroughNode("source", "target", 2, 15);
const result = createPassThroughNode("source-target", "section1", 2, 15);
expect(result).toEqual({
passThrough: true,
id: "source-target-2",
id: "source-target-section1-2",
value: 15,
depth: 2,
sourceId: "source",
targetId: "target",
});
});
});

727
yarn.lock

File diff suppressed because it is too large Load Diff