Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caa9871132 | ||
|
|
b069a362e4 | ||
|
|
2e5814ad0d |
No files matched your search
@@ -221,14 +221,7 @@ const personEntity = (person: DemoPerson, now: number): EntityInput => {
|
||||
const personHistory = (person: DemoPerson, now: number): EntityHistoryState[] =>
|
||||
person.timeline.map(([hoursAgo, state]) => {
|
||||
const time = (now - hoursAgo * HOUR) / 1000;
|
||||
const zoneId =
|
||||
state === "home" ? "home" : zones.find((zone) => zone.name === state)?.id;
|
||||
return {
|
||||
s: state,
|
||||
a: { in_zones: zoneId ? [`zone.${zoneId}`] : [] },
|
||||
lu: time,
|
||||
lc: time,
|
||||
};
|
||||
return { s: state, a: {}, lu: time, lc: time };
|
||||
});
|
||||
|
||||
export const mockMap = (hass: MockHomeAssistant) => {
|
||||
|
||||
@@ -6,10 +6,12 @@ import "../../../src/components/ha-settings-row";
|
||||
import "../../../src/components/ha-switch";
|
||||
import type { HaSwitch } from "../../../src/components/ha-switch";
|
||||
import "../../../src/components/ha-theme-settings";
|
||||
import type { ThemeSettings } from "../../../src/types";
|
||||
import type { HomeAssistant, ThemeSettings } from "../../../src/types";
|
||||
|
||||
@customElement("gallery-settings")
|
||||
class GallerySettings extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public themeSettings!: ThemeSettings;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@@ -24,6 +26,7 @@ class GallerySettings extends LitElement {
|
||||
Configure how the gallery renders component previews and examples.
|
||||
</div>
|
||||
<ha-theme-settings
|
||||
.hass=${this.hass}
|
||||
.selectedTheme=${this.themeSettings}
|
||||
.narrow=${this.narrow}
|
||||
.heading=${"Theme"}
|
||||
|
||||
@@ -242,6 +242,7 @@ class HaGallery extends LitElement {
|
||||
${
|
||||
isSettingsPage
|
||||
? html`<gallery-settings
|
||||
.hass=${this._galleryHass}
|
||||
.themeSettings=${this._themeSettings}
|
||||
.narrow=${this._narrow}
|
||||
.rtl=${this._rtl}
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@typescript/native": "npm:[email protected]",
|
||||
"@vitest/coverage-v8": "5.0.4",
|
||||
"@vitest/coverage-v8": "5.0.3",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-polyfill-corejs3": "1.0.0",
|
||||
"browserslist": "4.29.3",
|
||||
@@ -220,7 +220,7 @@
|
||||
"typescript-eslint": "8.71.1",
|
||||
"vite": "8.3.4",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "5.0.4",
|
||||
"vitest": "5.0.3",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "7.4.1"
|
||||
|
||||
@@ -576,8 +576,8 @@ importers:
|
||||
specifier: npm:[email protected]
|
||||
version: [email protected]
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 5.0.4
|
||||
version: 5.0.4([email protected].4)
|
||||
specifier: 5.0.3
|
||||
version: 5.0.3([email protected].3)
|
||||
babel-loader:
|
||||
specifier: 10.1.1
|
||||
version: 10.1.1(@babel/[email protected])(@rspack/[email protected](@swc/[email protected]))
|
||||
@@ -717,8 +717,8 @@ importers:
|
||||
specifier: 6.1.1
|
||||
version: 6.1.1([email protected])([email protected])([email protected](@types/[email protected])([email protected])([email protected]))
|
||||
vitest:
|
||||
specifier: 5.0.4
|
||||
version: 5.0.4(@types/[email protected])(@vitest/[email protected].4)([email protected])([email protected](@types/[email protected])([email protected])([email protected]))
|
||||
specifier: 5.0.3
|
||||
version: 5.0.3(@types/[email protected])(@vitest/[email protected].3)([email protected])([email protected](@types/[email protected])([email protected])([email protected]))
|
||||
webpack-stats-plugin:
|
||||
specifier: 1.1.3
|
||||
version: 1.1.3
|
||||
@@ -3738,27 +3738,27 @@ packages:
|
||||
'@vibrant/[email protected]':
|
||||
resolution: {integrity: sha512-Q/R6PYhSMWCXEk/IcXbWIzIu7Z4b58ABkGvcdF8Y+q/7g+KnpxKW5x/jfQ/6ciyYSby13wZZoEdNr3QQVgsdBQ==}
|
||||
|
||||
'@vitest/[email protected].4':
|
||||
resolution: {integrity: sha512-P5YcQY8Y6RmoE9xRXI38rT8pssbcubEzqtYTM2XQoxOXMDn04nRaTI1SM8qCl9/NfW3ag9GlzdSXu7IwO5Ft8A==}
|
||||
'@vitest/[email protected].3':
|
||||
resolution: {integrity: sha512-+klsyz7BvT1vCU28Zkfzms1Ia78XD0V41U3FUtRaa3S+vOr/EXvK1O6BvVD7l1RzEjm6SONR2aybOvDDf9u0mQ==}
|
||||
peerDependencies:
|
||||
'@vitest/browser': 5.0.4
|
||||
vitest: 5.0.4
|
||||
'@vitest/browser': 5.0.3
|
||||
vitest: 5.0.3
|
||||
peerDependenciesMeta:
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
|
||||
'@vitest/[email protected].2':
|
||||
resolution: {integrity: sha512-9J/JMwOf9AoJhAywhrn7ScKTL38hsWQP/qPG60OtaAFcQ5OXPwKsxZFlbnuCKmZ61m8/lGgHYnFpdyQZUvG/iA==}
|
||||
'@vitest/[email protected].1':
|
||||
resolution: {integrity: sha512-k3DJZ8LhMBK9NS4SclF1ASD3OgXEWDorbIcPTRDK0/Zae6fRvu+fJRxtFdLfHsa9Y24beCdPnoNZ4LviTNstfA==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@vitest/[email protected].2':
|
||||
resolution: {integrity: sha512-gUsfXZJbzPamoIY5TvHFiMMoXESBrUMo+xqaj+rYrWI69+EnvRlYBlP96ZnHPY4vX8kyUpgAnFUCg5wZG/HkDQ==}
|
||||
'@vitest/[email protected].1':
|
||||
resolution: {integrity: sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@vitest/[email protected].4':
|
||||
resolution: {integrity: sha512-tqAlrYWce4iUxhr3ncNohejtCTOcHeE8XWrSNZY3OhPoDn01yeGv+gnaBy64ATp7o6pb03eIbBAjzmibpc9vIQ==}
|
||||
'@vitest/[email protected].3':
|
||||
resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9 || ^3.0.0
|
||||
msw: ^2.4.9
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
msw:
|
||||
@@ -3766,8 +3766,8 @@ packages:
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/[email protected].4':
|
||||
resolution: {integrity: sha512-MZKk/gSkm2q+ICobMxCpH1mA+cCAWm8WV7UhxlqGjq/sJ3AGc1BqFOs5CRVXxH78wQtme9q/NAAgBquj4AcyMw==}
|
||||
'@vitest/[email protected].3':
|
||||
resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==}
|
||||
|
||||
'@vscode/[email protected]':
|
||||
resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==}
|
||||
@@ -3955,8 +3955,8 @@ packages:
|
||||
resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected].7:
|
||||
resolution: {integrity: sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==}
|
||||
[email protected].5:
|
||||
resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-j0s3bzYq9yKIVLKGE/tWlCpa3PfFLcrDZLTSVdnnCTGagXuXBJO4SsY9Xdk/fQBirCkH4evW5xOeJXqlAQFdsw==}
|
||||
@@ -4192,8 +4192,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-zfjJo4rM0+fUomGDBW/xcDjhIwz/210DGvip2MAMDZ8KHcRPnOHmEgHPZP0UHlZoxr8fYKVJOqcORhYQcG4FKQ==}
|
||||
|
||||
chai@6.3.0:
|
||||
resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==}
|
||||
chai@6.2.2:
|
||||
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
@@ -5893,11 +5893,11 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
magic-string@1.4.3:
|
||||
resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==}
|
||||
magic-string@1.2.3:
|
||||
resolution: {integrity: sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==}
|
||||
|
||||
[email protected].5:
|
||||
resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==}
|
||||
[email protected].4:
|
||||
resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==}
|
||||
@@ -6246,6 +6246,10 @@ packages:
|
||||
resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
@@ -6961,8 +6965,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
||||
|
||||
std-env@4.3.0:
|
||||
resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==}
|
||||
std-env@4.2.0:
|
||||
resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
|
||||
@@ -7158,8 +7162,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==}
|
||||
|
||||
[email protected].2:
|
||||
resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==}
|
||||
[email protected].0:
|
||||
resolution: {integrity: sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
[email protected]:
|
||||
@@ -7180,8 +7184,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==}
|
||||
|
||||
tinyrainbow@3.2.0:
|
||||
resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==}
|
||||
tinyrainbow@3.1.1:
|
||||
resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
[email protected]:
|
||||
@@ -7504,20 +7508,20 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
[email protected].4:
|
||||
resolution: {integrity: sha512-z0WxZRV2Sg48wF0FSgMq3TFG8JS38/Qr96vZVZdX6+Em9raQTvnXQvdTFJe2m3rfyJnEHeHQ/snceIdi8D1pVg==}
|
||||
[email protected].3:
|
||||
resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==}
|
||||
engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@opentelemetry/api': ^1.9.0
|
||||
'@types/node': ^22.0.0 || >=24.0.0
|
||||
'@vitest/browser-playwright': 5.0.4
|
||||
'@vitest/browser-preview': 5.0.4
|
||||
'@vitest/browser-playwright': 5.0.3
|
||||
'@vitest/browser-preview': 5.0.3
|
||||
'@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0
|
||||
'@vitest/coverage-istanbul': 5.0.4
|
||||
'@vitest/coverage-v8': 5.0.4
|
||||
'@vitest/ui': 5.0.4
|
||||
'@vitest/coverage-istanbul': 5.0.3
|
||||
'@vitest/coverage-v8': 5.0.3
|
||||
'@vitest/ui': 5.0.3
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
vite: ^6.4.0 || ^7.0.0 || ^8.0.0
|
||||
@@ -11348,34 +11352,34 @@ snapshots:
|
||||
dependencies:
|
||||
'@vibrant/types': 4.0.4
|
||||
|
||||
'@vitest/[email protected].4([email protected].4)':
|
||||
'@vitest/[email protected].3([email protected].3)':
|
||||
dependencies:
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
'@vitest/istanbul-lib-coverage': 1.0.2
|
||||
'@vitest/istanbul-lib-report': 1.0.2
|
||||
ast-v8-to-istanbul: 1.0.7
|
||||
magicast: 0.5.5
|
||||
obug: 3.0.0
|
||||
std-env: 4.3.0
|
||||
tinyrainbow: 3.2.0
|
||||
vitest: 5.0.4(@types/[email protected])(@vitest/[email protected].4)([email protected])([email protected](@types/[email protected])([email protected])([email protected]))
|
||||
'@vitest/istanbul-lib-coverage': 1.0.1
|
||||
'@vitest/istanbul-lib-report': 1.0.1
|
||||
ast-v8-to-istanbul: 1.0.5
|
||||
magicast: 0.5.4
|
||||
obug: 2.1.4
|
||||
std-env: 4.2.0
|
||||
tinyrainbow: 3.1.1
|
||||
vitest: 5.0.3(@types/[email protected])(@vitest/[email protected].3)([email protected])([email protected](@types/[email protected])([email protected])([email protected]))
|
||||
|
||||
'@vitest/[email protected].2': {}
|
||||
'@vitest/[email protected].1': {}
|
||||
|
||||
'@vitest/[email protected].2':
|
||||
'@vitest/[email protected].1':
|
||||
dependencies:
|
||||
'@vitest/istanbul-lib-coverage': 1.0.2
|
||||
'@vitest/istanbul-lib-coverage': 1.0.1
|
||||
|
||||
'@vitest/[email protected].4([email protected](@types/[email protected])([email protected])([email protected]))':
|
||||
'@vitest/[email protected].3([email protected](@types/[email protected])([email protected])([email protected]))':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
'@vitest/spy': 5.0.4
|
||||
'@vitest/spy': 5.0.3
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 1.4.3
|
||||
magic-string: 1.2.3
|
||||
optionalDependencies:
|
||||
vite: 8.3.4(@types/[email protected])([email protected])([email protected])
|
||||
|
||||
'@vitest/[email protected].4': {}
|
||||
'@vitest/[email protected].3': {}
|
||||
|
||||
'@vscode/[email protected]': {}
|
||||
|
||||
@@ -11535,7 +11539,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected].7:
|
||||
[email protected].5:
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
estree-walker: 3.0.3
|
||||
@@ -11801,7 +11805,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
chai@6.3.0: {}
|
||||
chai@6.2.2: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
@@ -13616,11 +13620,11 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.6.0
|
||||
|
||||
magic-string@1.4.3:
|
||||
magic-string@1.2.3:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.6.0
|
||||
|
||||
[email protected].5:
|
||||
[email protected].4:
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.8
|
||||
'@babel/types': 7.29.8
|
||||
@@ -13731,7 +13735,7 @@ snapshots:
|
||||
acorn: 8.18.0
|
||||
html-minifier-next: 8.1.0
|
||||
lightningcss: 1.33.0
|
||||
magic-string: 1.4.3
|
||||
magic-string: 1.2.3
|
||||
transitivePeerDependencies:
|
||||
- '@swc/core'
|
||||
|
||||
@@ -14027,6 +14031,8 @@ snapshots:
|
||||
dependencies:
|
||||
isobject: 3.0.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -14856,7 +14862,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
std-env@4.3.0: {}
|
||||
std-env@4.2.0: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
@@ -15065,7 +15071,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected].2: {}
|
||||
[email protected].0: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -15080,7 +15086,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
tinyrainbow@3.2.0: {}
|
||||
tinyrainbow@3.1.1: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -15433,25 +15439,25 @@ snapshots:
|
||||
terser: 5.50.0
|
||||
yaml: 2.9.1
|
||||
|
||||
[email protected].4(@types/[email protected])(@vitest/[email protected].4)([email protected])([email protected](@types/[email protected])([email protected])([email protected])):
|
||||
[email protected].3(@types/[email protected])(@vitest/[email protected].3)([email protected])([email protected](@types/[email protected])([email protected])([email protected])):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/mocker': 5.0.4([email protected](@types/[email protected])([email protected])([email protected]))
|
||||
chai: 6.3.0
|
||||
'@vitest/mocker': 5.0.3([email protected](@types/[email protected])([email protected])([email protected]))
|
||||
chai: 6.2.2
|
||||
es-module-lexer: 2.3.2
|
||||
expect-type: 1.4.0
|
||||
magic-string: 1.4.3
|
||||
obug: 3.0.0
|
||||
magic-string: 1.2.3
|
||||
obug: 2.1.4
|
||||
picomatch: 4.0.7
|
||||
std-env: 4.3.0
|
||||
tinybench: 6.2.2
|
||||
std-env: 4.2.0
|
||||
tinybench: 6.2.0
|
||||
tinyexec: 1.3.1
|
||||
tinyglobby: 0.2.17
|
||||
vite: 8.3.4(@types/[email protected])([email protected])([email protected])
|
||||
why-is-node-running: 3.2.1
|
||||
optionalDependencies:
|
||||
'@types/node': 24.19.2
|
||||
'@vitest/coverage-v8': 5.0.4([email protected].4)
|
||||
'@vitest/coverage-v8': 5.0.3([email protected].3)
|
||||
jsdom: 30.1.2
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<svg width="94" height="28" viewBox="0 0 94 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="28" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="27" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="8" y="8" width="78" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="12" y="13.25" width="6" height="1.5" rx="0.75" fill="black" fill-opacity="0.48"/>
|
||||
<rect x="37" y="12.5" width="20" height="3" rx="1.5" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="76" y="13.25" width="6" height="1.5" rx="0.75" fill="black" fill-opacity="0.48"/>
|
||||
<rect x="78.25" y="11" width="1.5" height="6" rx="0.75" fill="black" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 691 B |
@@ -1,9 +0,0 @@
|
||||
<svg width="94" height="28" viewBox="0 0 94 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="28" rx="8" fill="#1C1C1C"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="27" rx="7.5" stroke="white" stroke-opacity="0.24"/>
|
||||
<rect x="8" y="8" width="78" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="12" y="13.25" width="6" height="1.5" rx="0.75" fill="white" fill-opacity="0.72"/>
|
||||
<rect x="37" y="12.5" width="20" height="3" rx="1.5" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="76" y="13.25" width="6" height="1.5" rx="0.75" fill="white" fill-opacity="0.72"/>
|
||||
<rect x="78.25" y="11" width="1.5" height="6" rx="0.75" fill="white" fill-opacity="0.72"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 693 B |
@@ -1,7 +0,0 @@
|
||||
<svg width="94" height="28" viewBox="0 0 94 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="28" rx="8" fill="white"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="27" rx="7.5" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="8" y="8" width="78" height="12" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="8" y="8" width="46" height="12" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="49" y="10.5" width="2" height="7" rx="1" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 478 B |
@@ -1,7 +0,0 @@
|
||||
<svg width="94" height="28" viewBox="0 0 94 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="94" height="28" rx="8" fill="#1C1C1C"/>
|
||||
<rect x="0.5" y="0.5" width="93" height="27" rx="7.5" stroke="white" stroke-opacity="0.24"/>
|
||||
<rect x="8" y="8" width="78" height="12" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="8" y="8" width="46" height="12" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="49" y="10.5" width="2" height="7" rx="1" fill="#1C1C1C"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 482 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="36" height="16" viewBox="0 0 36 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 252 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="36" height="16" viewBox="0 0 36 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 252 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 330 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 330 B |
@@ -1,6 +0,0 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 408 B |
@@ -1,6 +0,0 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="16" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="20" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 408 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="36" height="16" viewBox="0 0 36 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 174 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="36" height="16" viewBox="0 0 36 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 174 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 252 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 252 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 330 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="40" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 330 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 174 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="56" height="16" viewBox="0 0 56 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 174 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="black" fill-opacity="0.12"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 252 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
<rect x="60" width="16" height="16" rx="3" fill="white" fill-opacity="0.24"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 252 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="76" height="16" rx="3" fill="black" fill-opacity="0.32"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 174 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="76" height="16" viewBox="0 0 76 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="76" height="16" rx="3" fill="white" fill-opacity="0.48"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 174 B |
@@ -7,7 +7,7 @@ export interface EntityLocation {
|
||||
gpsAccuracy?: number;
|
||||
}
|
||||
|
||||
export const findFirstActiveZone = (
|
||||
const findFirstActiveZone = (
|
||||
inZones: readonly string[],
|
||||
states: HassEntities
|
||||
): HassEntity | undefined => {
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
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 { consume } from "../../common/decorators/consume";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import type { Condition } from "../../data/automation";
|
||||
import { subscribeCondition } from "../../data/automation";
|
||||
import { connectionContext } from "../../data/context";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-tooltip";
|
||||
import "./ha-automation-row-live-test";
|
||||
import type { LiveTestState } from "./ha-automation-row-live-test";
|
||||
|
||||
@customElement("ha-automation-condition-live-test")
|
||||
export class HaAutomationConditionLiveTest extends LitElement {
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection!: ContextType<typeof connectionContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public condition!: Condition;
|
||||
|
||||
@@ -58,7 +51,7 @@ export class HaAutomationConditionLiveTest extends LitElement {
|
||||
<slot></slot>
|
||||
<ha-automation-row-live-test
|
||||
.state=${this._liveTestResult.state}
|
||||
.label=${this._localize(
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.live_test_state.${this._liveTestResult.state}`
|
||||
)}
|
||||
></ha-automation-row-live-test>
|
||||
@@ -76,7 +69,7 @@ export class HaAutomationConditionLiveTest extends LitElement {
|
||||
private _resetSubscription() {
|
||||
this._liveTestResult = {
|
||||
state: "unknown",
|
||||
message: this._localize(
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.live_test_state.unknown"
|
||||
),
|
||||
};
|
||||
@@ -99,14 +92,14 @@ export class HaAutomationConditionLiveTest extends LitElement {
|
||||
}
|
||||
|
||||
const conditionUnsub = subscribeCondition(
|
||||
this._connection.connection,
|
||||
this.hass.connection,
|
||||
(result) => {
|
||||
if (result.error) {
|
||||
this._handleLiveTestError(result.error);
|
||||
} else {
|
||||
this._liveTestResult = {
|
||||
state: result.result ? "pass" : "fail",
|
||||
message: this._localize(
|
||||
message: this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.testing_${result.result ? "pass" : "error"}`
|
||||
),
|
||||
};
|
||||
@@ -128,7 +121,7 @@ export class HaAutomationConditionLiveTest extends LitElement {
|
||||
typeof error !== "string" && error.code === "invalid_format";
|
||||
this._liveTestResult = {
|
||||
state: invalid ? "invalid" : "unknown",
|
||||
message: this._localize(
|
||||
message: this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.${invalid ? "invalid_condition" : "live_test_state.unknown"}`
|
||||
),
|
||||
};
|
||||
|
||||
@@ -43,10 +43,7 @@ import {
|
||||
findAvailableLanguage,
|
||||
getTranslation,
|
||||
} from "../util/common-translation";
|
||||
import {
|
||||
documentationUrl,
|
||||
SECURE_CONNECTION_DOCUMENTATION_PATH,
|
||||
} from "../util/documentation-url";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-alert";
|
||||
import "./ha-markdown";
|
||||
import "./input/ha-input";
|
||||
@@ -54,7 +51,7 @@ import type { HaInput } from "./input/ha-input";
|
||||
|
||||
interface AssistMessage {
|
||||
who: string;
|
||||
text: string;
|
||||
text: string | TemplateResult;
|
||||
thinking: string;
|
||||
thinking_expanded?: boolean;
|
||||
tool_calls: Record<
|
||||
@@ -542,18 +539,31 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre>
|
||||
this._conversation = [...this._conversation!, message];
|
||||
}
|
||||
|
||||
private _showNotSupportedMessage() {
|
||||
const documentationLink = `[${this._localize(
|
||||
"ui.components.secure_connection_required.documentation_link"
|
||||
)}](${documentationUrl(this._config, SECURE_CONNECTION_DOCUMENTATION_PATH)})`;
|
||||
private async _showNotSupportedMessage() {
|
||||
this._addMessage({
|
||||
who: "hass",
|
||||
text: `${this._localize(
|
||||
"ui.components.secure_connection_required.microphone"
|
||||
)}\n\n${this._localize(
|
||||
"ui.components.secure_connection_required.learn_more",
|
||||
{ documentation_link: documentationLink }
|
||||
)}`,
|
||||
text:
|
||||
// New lines matter for messages
|
||||
// prettier-ignore
|
||||
html`${this._localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_browser"
|
||||
)}
|
||||
|
||||
${this._localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_documentation",
|
||||
{
|
||||
documentation_link: html`<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href=${documentationUrl(
|
||||
this._config,
|
||||
"/docs/configuration/securing/#remote-access"
|
||||
)}
|
||||
>${this._localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_documentation_link"
|
||||
)}</a>`,
|
||||
}
|
||||
)}`,
|
||||
thinking: "",
|
||||
tool_calls: {},
|
||||
});
|
||||
@@ -573,31 +583,13 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre>
|
||||
}
|
||||
this._stt_binary_handler_id = undefined;
|
||||
this._audioBuffer = [];
|
||||
|
||||
const microphoneError = await this._audioRecorder.start();
|
||||
|
||||
if (microphoneError) {
|
||||
this._audioBuffer = undefined;
|
||||
this._processing = false;
|
||||
this._addMessage({
|
||||
who: "hass",
|
||||
text: this._localize(
|
||||
`ui.dialogs.voice_command.microphone_${microphoneError}`
|
||||
),
|
||||
thinking: "",
|
||||
tool_calls: {},
|
||||
error: true,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const userMessage: AssistMessage = {
|
||||
who: "user",
|
||||
text: "…",
|
||||
thinking: "",
|
||||
tool_calls: {},
|
||||
};
|
||||
await this._audioRecorder.start();
|
||||
|
||||
this._addMessage(userMessage);
|
||||
|
||||
@@ -610,9 +602,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre>
|
||||
if (event.type === "run-start") {
|
||||
this._stt_binary_handler_id =
|
||||
event.data.runner_data.stt_binary_handler_id;
|
||||
this._audio = new Audio(
|
||||
this._connection.hassUrl(event.data.tts_output!.url)
|
||||
);
|
||||
this._audio = new Audio(event.data.tts_output!.url);
|
||||
this._audio.play();
|
||||
this._audio.addEventListener("ended", () => {
|
||||
this._unloadAudio();
|
||||
@@ -626,11 +616,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre>
|
||||
);
|
||||
this._audio.addEventListener("error", () => {
|
||||
this._unloadAudio();
|
||||
showAlertDialog(this, {
|
||||
title: this._localize(
|
||||
"ui.dialogs.voice_command.error_playing_audio"
|
||||
),
|
||||
});
|
||||
showAlertDialog(this, { title: "Error playing audio." });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -680,9 +666,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre>
|
||||
);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
title: this._localize(
|
||||
"ui.dialogs.voice_command.error_starting_pipeline"
|
||||
),
|
||||
title: "Error starting pipeline",
|
||||
text: err.message || err,
|
||||
});
|
||||
this._stopListening();
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { configContext } from "../data/context";
|
||||
import "./ha-icon-next";
|
||||
import "./ha-svg-icon";
|
||||
import "./item/ha-list-item-button";
|
||||
import "./list/ha-list-nav";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-config-navigation-list")
|
||||
class HaConfigNavigationList extends LitElement {
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@@ -66,7 +63,7 @@ class HaConfigNavigationList extends LitElement {
|
||||
}
|
||||
|
||||
private _handleExternalApp() {
|
||||
this._config.auth.external!.fireMessage({ type: "config_screen/show" });
|
||||
this.hass.auth.external!.fireMessage({ type: "config_screen/show" });
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
|
||||
@@ -20,7 +20,6 @@ import "./ha-button";
|
||||
import "./ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-secure-connection-alert";
|
||||
import "./ha-spinner";
|
||||
import "./input/ha-input";
|
||||
import type { HaInput } from "./input/ha-input";
|
||||
@@ -164,15 +163,15 @@ class HaQrScanner extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
</div>`
|
||||
: html`${
|
||||
!window.isSecureContext
|
||||
? html`<ha-secure-connection-alert
|
||||
feature="camera"
|
||||
></ha-secure-connection-alert>`
|
||||
: html`<ha-alert alert-type="warning">
|
||||
${this._localize("ui.components.qr-scanner.not_supported")}
|
||||
</ha-alert>`
|
||||
}
|
||||
: html`<ha-alert alert-type="warning">
|
||||
${
|
||||
!window.isSecureContext
|
||||
? this._localize(
|
||||
"ui.components.qr-scanner.only_https_supported"
|
||||
)
|
||||
: this._localize("ui.components.qr-scanner.not_supported")
|
||||
}
|
||||
</ha-alert>
|
||||
<p>${this._localize("ui.components.qr-scanner.manual_input")}</p>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { configContext } from "../data/context";
|
||||
import {
|
||||
documentationUrl,
|
||||
SECURE_CONNECTION_DOCUMENTATION_PATH,
|
||||
} from "../util/documentation-url";
|
||||
import "./ha-alert";
|
||||
|
||||
export type SecureConnectionFeature =
|
||||
"microphone" | "camera" | "push_notifications";
|
||||
|
||||
@customElement("ha-secure-connection-alert")
|
||||
class HaSecureConnectionAlert extends LitElement {
|
||||
@property() public feature!: SecureConnectionFeature;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
protected render() {
|
||||
if (!this._localize || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-alert alert-type="warning">
|
||||
${this._localize(
|
||||
`ui.components.secure_connection_required.${this.feature}`
|
||||
)}
|
||||
${this._localize(
|
||||
"ui.components.secure_connection_required.learn_more",
|
||||
{
|
||||
documentation_link: html`<a
|
||||
href=${documentationUrl(
|
||||
this._config,
|
||||
SECURE_CONNECTION_DOCUMENTATION_PATH
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>${this._localize(
|
||||
"ui.components.secure_connection_required.documentation_link"
|
||||
)}</a
|
||||
>`,
|
||||
}
|
||||
)}
|
||||
</ha-alert>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-secure-connection-alert": HaSecureConnectionAlert;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ConfigEntry } from "../../data/config_entries";
|
||||
import { getConfigEntries } from "../../data/config_entries";
|
||||
import { apiContext, registriesContext } from "../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
||||
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
|
||||
import type { AreaSelector } from "../../data/selector";
|
||||
@@ -23,11 +20,7 @@ import "../ha-areas-picker";
|
||||
|
||||
@customElement("ha-selector-area")
|
||||
export class HaAreaSelector extends LitElement {
|
||||
@consume({ context: registriesContext, subscribe: true })
|
||||
private _registries!: ContextType<typeof registriesContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: AreaSelector;
|
||||
|
||||
@@ -84,7 +77,7 @@ export class HaAreaSelector extends LitElement {
|
||||
protected updated(): void {
|
||||
if (!this._fetchedConfigEntries && this._hasIntegration(this.selector)) {
|
||||
this._fetchedConfigEntries = true;
|
||||
getConfigEntries(this._api)
|
||||
getConfigEntries(this.hass)
|
||||
.then((entries) => {
|
||||
this._configEntries = entries;
|
||||
})
|
||||
@@ -147,8 +140,8 @@ export class HaAreaSelector extends LitElement {
|
||||
filter,
|
||||
entity,
|
||||
undefined,
|
||||
this._registries.entities,
|
||||
this._registries.devices
|
||||
this.hass.entities,
|
||||
this.hass.devices
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -160,8 +153,8 @@ export class HaAreaSelector extends LitElement {
|
||||
|
||||
const deviceIntegrations = this._hasIntegration(this.selector)
|
||||
? this._deviceIntegrationLookup(
|
||||
this._registries.entities,
|
||||
this._registries.devices,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this._configEntries
|
||||
)
|
||||
: undefined;
|
||||
|
||||
@@ -134,7 +134,7 @@ export class HaNumberSelector extends LitElement {
|
||||
.required=${this.required}
|
||||
.validationMessage=${this.selector.number?.validation_message}
|
||||
type="number"
|
||||
auto-validate
|
||||
autoValidate
|
||||
.withoutSpinButtons=${!isBox}
|
||||
@input=${this._handleInputChange}
|
||||
>
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, nothing, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { configContext, connectionContext } from "../../data/context";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import "../ha-code-editor";
|
||||
import "../ha-input-helper-text";
|
||||
@@ -21,14 +17,7 @@ const WARNING_STRINGS = ["template:", "sensor:", "state:", "trigger: template"];
|
||||
|
||||
@customElement("ha-selector-template")
|
||||
export class HaTemplateSelector extends LitElement {
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection!: ContextType<typeof connectionContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: TemplateSelector;
|
||||
|
||||
@@ -90,7 +79,7 @@ export class HaTemplateSelector extends LitElement {
|
||||
${
|
||||
this.warn
|
||||
? html`<ha-alert alert-type="warning"
|
||||
>${this._localize(
|
||||
>${this.hass.localize(
|
||||
"ui.components.selectors.template.yaml_warning",
|
||||
{ string: this.warn }
|
||||
)}
|
||||
@@ -99,10 +88,10 @@ export class HaTemplateSelector extends LitElement {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href=${documentationUrl(
|
||||
this._config,
|
||||
this.hass,
|
||||
"/docs/configuration/templating/"
|
||||
)}
|
||||
>${this._localize(
|
||||
>${this.hass.localize(
|
||||
"ui.components.selectors.template.learn_more"
|
||||
)}</a
|
||||
></ha-alert
|
||||
@@ -165,7 +154,7 @@ ${
|
||||
|
||||
try {
|
||||
this._unsubRenderTemplate = subscribeRenderTemplate(
|
||||
this._connection.connection,
|
||||
this.hass.connection,
|
||||
(result) => {
|
||||
if ("error" in result) {
|
||||
// We show the latest error, or a warning if there are no errors
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { normalizeLuminance } from "../common/color/palette";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { uiContext } from "../data/context";
|
||||
import {
|
||||
DefaultAccentColor,
|
||||
DefaultPrimaryColor,
|
||||
} from "../resources/theme/color/color.globals";
|
||||
import type { ThemeSettings, ValueChangedEvent } from "../types";
|
||||
import type { HomeAssistant, ThemeSettings, ValueChangedEvent } from "../types";
|
||||
import "./ha-button";
|
||||
import "./item/ha-list-item-base";
|
||||
import "./list/ha-list-base";
|
||||
@@ -37,9 +34,7 @@ export interface ThemeSettingsLabels {
|
||||
|
||||
@customElement("ha-theme-settings")
|
||||
export class HaThemeSettings extends LitElement {
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
private _ui!: ContextType<typeof uiContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selectedTheme?: ThemeSettings | null;
|
||||
|
||||
@@ -61,19 +56,19 @@ export class HaThemeSettings extends LitElement {
|
||||
public themePickerDisabled = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const themeSettings = this.selectedTheme ?? this._ui.selectedTheme;
|
||||
const themeSettings = this.selectedTheme ?? this.hass.selectedTheme;
|
||||
const curThemeIsUseDefault = themeSettings?.theme === "";
|
||||
const curTheme = themeSettings?.theme
|
||||
? themeSettings.theme
|
||||
: this._ui.themes.darkMode
|
||||
? this._ui.themes.default_dark_theme || this._ui.themes.default_theme
|
||||
: this._ui.themes.default_theme;
|
||||
: this.hass.themes.darkMode
|
||||
? this.hass.themes.default_dark_theme || this.hass.themes.default_theme
|
||||
: this.hass.themes.default_theme;
|
||||
|
||||
const showDarkMode =
|
||||
curTheme === HOME_ASSISTANT_THEME ||
|
||||
(curThemeIsUseDefault &&
|
||||
this._ui.themes.default_dark_theme &&
|
||||
this._ui.themes.default_theme) ||
|
||||
this.hass.themes.default_dark_theme &&
|
||||
this.hass.themes.default_theme) ||
|
||||
this._supportsModeSelection(curTheme);
|
||||
|
||||
return html`
|
||||
@@ -208,7 +203,7 @@ export class HaThemeSettings extends LitElement {
|
||||
}
|
||||
|
||||
private _supportsModeSelection(themeName: string): boolean {
|
||||
const theme = this._ui.themes.themes[themeName];
|
||||
const theme = this.hass.themes.themes[themeName];
|
||||
if (!theme) {
|
||||
return false;
|
||||
}
|
||||
@@ -236,7 +231,7 @@ export class HaThemeSettings extends LitElement {
|
||||
const theme = ev.detail.value;
|
||||
|
||||
if (theme === undefined) {
|
||||
if (this.selectedTheme?.theme || this._ui.selectedTheme?.theme) {
|
||||
if (this.selectedTheme?.theme || this.hass.selectedTheme?.theme) {
|
||||
fireEvent(this, "theme-settings-changed", {
|
||||
theme: "",
|
||||
primaryColor: undefined,
|
||||
@@ -246,7 +241,7 @@ export class HaThemeSettings extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (theme === (this.selectedTheme ?? this._ui.selectedTheme)?.theme) {
|
||||
if (theme === (this.selectedTheme ?? this.hass.selectedTheme)?.theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type LitElement, type PropertyValues, css } from "lit";
|
||||
import { type LitElement, css } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { supportsNativeElementInternals } from "../../common/feature-detect/support-native-element-internals";
|
||||
@@ -23,7 +23,6 @@ export interface WaInput {
|
||||
): void;
|
||||
checkValidity(): boolean;
|
||||
validationMessage: string;
|
||||
updateComplete?: Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface WaInputMixinInterface {
|
||||
@@ -227,18 +226,6 @@ export const WaInputMixin = <T extends Constructor<LitElement>>(
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (this._invalid && changedProperties.has("value")) {
|
||||
this._revalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private async _revalidate(): Promise<void> {
|
||||
await this._formControl?.updateComplete;
|
||||
this._invalid = !this.checkValidity();
|
||||
}
|
||||
|
||||
protected _handleInvalid(): void {
|
||||
// Polyfilled internals dispatch `invalid` themselves, so only trust the
|
||||
// event when validity comes from the platform (#51338).
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ASSIST_ENTITIES, SENSOR_ENTITIES } from "../../common/const";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import type { EntityCategory } from "../entity/entity_registry";
|
||||
|
||||
export const DEVICE_ENTITY_CATEGORIES = [
|
||||
"control",
|
||||
"sensor",
|
||||
"notify",
|
||||
"event",
|
||||
"assist",
|
||||
"config",
|
||||
"diagnostic",
|
||||
] as const;
|
||||
|
||||
export type DeviceEntityCategory = (typeof DEVICE_ENTITY_CATEGORIES)[number];
|
||||
|
||||
export const computeDeviceEntityCategory = (entry: {
|
||||
entity_id: string;
|
||||
entity_category?: EntityCategory | null;
|
||||
}): DeviceEntityCategory => {
|
||||
const domain = computeDomain(entry.entity_id);
|
||||
|
||||
if (ASSIST_ENTITIES.includes(domain)) {
|
||||
return "assist";
|
||||
}
|
||||
|
||||
if (domain === "event" || domain === "notify") {
|
||||
return domain;
|
||||
}
|
||||
|
||||
if (entry.entity_category) {
|
||||
return entry.entity_category;
|
||||
}
|
||||
|
||||
if (SENSOR_ENTITIES.includes(domain)) {
|
||||
return "sensor";
|
||||
}
|
||||
|
||||
return "control";
|
||||
};
|
||||
@@ -10,7 +10,7 @@ import type { LightColor } from "../light";
|
||||
import type { NextNamePart, RegistryEntry } from "../registry";
|
||||
import type { Segment } from "../vacuum";
|
||||
|
||||
type EntityCategory = "config" | "diagnostic";
|
||||
export type EntityCategory = "config" | "diagnostic";
|
||||
|
||||
export interface EntityRegistryDisplayEntry {
|
||||
entity_id: string;
|
||||
|
||||
@@ -104,16 +104,13 @@ export const fetchDateWS = (
|
||||
hass: Pick<HomeAssistant, "states" | "callWS">,
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
entityIds: string[],
|
||||
options: {
|
||||
minimalResponse?: boolean;
|
||||
} = {}
|
||||
entityIds: string[]
|
||||
) => {
|
||||
const params = {
|
||||
type: "history/history_during_period",
|
||||
start_time: startTime.toISOString(),
|
||||
end_time: endTime.toISOString(),
|
||||
minimal_response: options.minimalResponse ?? true,
|
||||
minimal_response: true,
|
||||
no_attributes: !entityIds.some((entityId) =>
|
||||
entityIdHistoryNeedsAttributes(hass, entityId)
|
||||
),
|
||||
|
||||
@@ -133,7 +133,8 @@ export class DialogEnterCode
|
||||
id="code"
|
||||
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
|
||||
type="password"
|
||||
auto-validate
|
||||
autoValidate
|
||||
validateOnInitialRender
|
||||
pattern=${ifDefined(this._dialogParams.codePattern)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
inputmode="text"
|
||||
|
||||
@@ -365,6 +365,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
? html`<ha-automation-condition-live-test
|
||||
id="condition-icon"
|
||||
slot="leading-icon"
|
||||
.hass=${this.hass}
|
||||
.condition=${liveTestCondition}
|
||||
>
|
||||
<ha-svg-icon
|
||||
|
||||
@@ -185,6 +185,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
? html`<ha-automation-condition-live-test
|
||||
id="condition-icon"
|
||||
slot="leading-icon"
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
>
|
||||
<ha-condition-icon
|
||||
|
||||
@@ -532,9 +532,6 @@ export class HaBackupOverviewProgress extends LitElement {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-list-item-base::part(end) {
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -145,6 +145,7 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
>
|
||||
<ha-card outlined>
|
||||
<ha-config-navigation-list
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.pages=${pages}
|
||||
has-secondary
|
||||
|
||||
@@ -106,6 +106,7 @@ class HaConfigNavigation extends LitElement {
|
||||
<div class="visually-hidden" role="heading" aria-level="2">${label}</div>
|
||||
<ha-config-navigation-list
|
||||
has-secondary
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.pages=${pages}
|
||||
.label=${label}
|
||||
|
||||
@@ -25,7 +25,6 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { ASSIST_ENTITIES, SENSOR_ENTITIES } from "../../../common/const";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
@@ -65,6 +64,11 @@ import {
|
||||
sortConfigEntries,
|
||||
} from "../../../data/config_entries";
|
||||
import { fireRelatedContext, fullEntitiesContext } from "../../../data/context";
|
||||
import {
|
||||
DEVICE_ENTITY_CATEGORIES,
|
||||
computeDeviceEntityCategory,
|
||||
type DeviceEntityCategory,
|
||||
} from "../../../data/device/device_entity_category";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import {
|
||||
removeDeviceFromRegistry,
|
||||
@@ -305,44 +309,11 @@ export class HaConfigDevicePage extends LitElement {
|
||||
|
||||
private _entitiesByCategory = memoizeOne(
|
||||
(entities: EntityRegistryEntry[]) => {
|
||||
const result = groupBy(entities, (entry) => {
|
||||
const domain = computeDomain(entry.entity_id);
|
||||
|
||||
if (ASSIST_ENTITIES.includes(domain)) {
|
||||
return "assist";
|
||||
}
|
||||
|
||||
if (domain === "event" || domain === "notify") {
|
||||
return domain;
|
||||
}
|
||||
|
||||
if (entry.entity_category) {
|
||||
return entry.entity_category;
|
||||
}
|
||||
|
||||
if (SENSOR_ENTITIES.includes(domain)) {
|
||||
return "sensor";
|
||||
}
|
||||
|
||||
return "control";
|
||||
}) as Record<
|
||||
| "control"
|
||||
| "event"
|
||||
| "sensor"
|
||||
| "assist"
|
||||
| "notify"
|
||||
| NonNullable<EntityRegistryEntry["entity_category"]>,
|
||||
const result = groupBy(entities, computeDeviceEntityCategory) as Record<
|
||||
DeviceEntityCategory,
|
||||
EntityRegistryStateEntry[]
|
||||
>;
|
||||
for (const key of [
|
||||
"assist",
|
||||
"config",
|
||||
"control",
|
||||
"diagnostic",
|
||||
"event",
|
||||
"notify",
|
||||
"sensor",
|
||||
]) {
|
||||
for (const key of DEVICE_ENTITY_CATEGORIES) {
|
||||
if (!(key in result)) {
|
||||
result[key] = [];
|
||||
}
|
||||
@@ -909,17 +880,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
`;
|
||||
|
||||
const entitiesColumn = html`
|
||||
${(
|
||||
[
|
||||
"control",
|
||||
"sensor",
|
||||
"notify",
|
||||
"event",
|
||||
"assist",
|
||||
"config",
|
||||
"diagnostic",
|
||||
] as const
|
||||
).map((category) =>
|
||||
${DEVICE_ENTITY_CATEGORIES.map((category) =>
|
||||
// Make sure we render controls if no other cards will be rendered
|
||||
entitiesByCategory[category].length > 0 ||
|
||||
(entities.length === 0 && category === "control")
|
||||
|
||||
@@ -664,13 +664,8 @@ export class HaConfigEntryRow extends LitElement {
|
||||
const locale_key = result.require_restart
|
||||
? "reload_restart_confirm"
|
||||
: "reload_confirm";
|
||||
// The row can move between sections when the entry state changes on reload
|
||||
const host =
|
||||
(this.isConnected
|
||||
? this
|
||||
: document.querySelector<HTMLElement>("home-assistant")) ?? this;
|
||||
showToast(host, {
|
||||
message: this.hass.localize(
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.${locale_key}`
|
||||
),
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@ import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../../../../../../types";
|
||||
|
||||
import "../../../../../../components/ha-alert";
|
||||
import "../../../../../../components/ha-icon-next";
|
||||
import "../../../../../../components/ha-secure-connection-alert";
|
||||
import "../../../../../../components/item/ha-list-item-button";
|
||||
import "../../../../../../components/list/ha-list-base";
|
||||
|
||||
@@ -19,9 +19,11 @@ export class ZWaveJsAddNodeSelectMethod extends LitElement {
|
||||
return html`
|
||||
${
|
||||
!this.hideQrWebcam && !window.isSecureContext
|
||||
? html`<ha-secure-connection-alert
|
||||
feature="camera"
|
||||
></ha-secure-connection-alert>`
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.select_method.webcam_unsupported"
|
||||
)}</ha-alert
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
<ha-list-base>
|
||||
|
||||
@@ -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, query, state } from "lit/decorators";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { extractSearchParam } from "../../../../common/url/search-params";
|
||||
@@ -7,7 +7,6 @@ import "../../../../components/ha-assist-pipeline-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-checkbox";
|
||||
import type { HaCheckbox } from "../../../../components/ha-checkbox";
|
||||
import "../../../../components/ha-secure-connection-alert";
|
||||
import "../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
import type {
|
||||
@@ -98,13 +97,6 @@ export class AssistPipelineRunDebug extends LitElement {
|
||||
.value=${this._pipelineId}
|
||||
@value-changed=${this._pipelinePicked}
|
||||
></ha-assist-pipeline-picker>
|
||||
${
|
||||
!window.isSecureContext
|
||||
? html`<ha-secure-connection-alert
|
||||
feature="microphone"
|
||||
></ha-secure-connection-alert>`
|
||||
: nothing
|
||||
}
|
||||
<div class="start-buttons">
|
||||
<ha-button
|
||||
appearance="filled"
|
||||
@@ -261,24 +253,6 @@ export class AssistPipelineRunDebug extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private async _startRecorder(audioRecorder: AudioRecorder) {
|
||||
const microphoneError = await audioRecorder.start();
|
||||
|
||||
if (!microphoneError) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this._audioBuffer = undefined;
|
||||
this._finished = true;
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
`ui.dialogs.voice_command.microphone_${microphoneError}`
|
||||
),
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async _runAudioWakeWordPipeline() {
|
||||
const audioRecorder = new AudioRecorder((data) => {
|
||||
if (this._audioBuffer) {
|
||||
@@ -289,10 +263,7 @@ export class AssistPipelineRunDebug extends LitElement {
|
||||
});
|
||||
|
||||
this._audioBuffer = [];
|
||||
|
||||
if (!(await this._startRecorder(audioRecorder))) {
|
||||
return;
|
||||
}
|
||||
await audioRecorder.start();
|
||||
|
||||
let run: PipelineRun | undefined;
|
||||
|
||||
@@ -335,7 +306,7 @@ export class AssistPipelineRunDebug extends LitElement {
|
||||
|
||||
// Play audio when we're done.
|
||||
if (updatedRun.stage === "done" && !updatedRun.error) {
|
||||
const url = this.hass.hassUrl(updatedRun.tts!.tts_output!.url);
|
||||
const url = updatedRun.tts!.tts_output!.url;
|
||||
const audio = new Audio(url);
|
||||
audio.addEventListener("ended", () => {
|
||||
if (
|
||||
@@ -375,10 +346,7 @@ export class AssistPipelineRunDebug extends LitElement {
|
||||
});
|
||||
|
||||
this._audioBuffer = [];
|
||||
|
||||
if (!(await this._startRecorder(audioRecorder))) {
|
||||
return;
|
||||
}
|
||||
await audioRecorder.start();
|
||||
|
||||
let run: PipelineRun | undefined;
|
||||
|
||||
@@ -418,7 +386,7 @@ export class AssistPipelineRunDebug extends LitElement {
|
||||
|
||||
// Play audio when we're done.
|
||||
if (updatedRun.stage === "done") {
|
||||
const url = this.hass.hassUrl(updatedRun.tts!.tts_output!.url);
|
||||
const url = updatedRun.tts!.tts_output!.url;
|
||||
const audio = new Audio(url);
|
||||
audio.addEventListener("ended", () => {
|
||||
if (
|
||||
@@ -567,10 +535,6 @@ export class AssistPipelineRunDebug extends LitElement {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-secure-connection-alert {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.start-row ha-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -590,7 +590,7 @@ export class AssistPipelineDebug extends LitElement {
|
||||
// Stop any existing audio first
|
||||
this._stopTTS();
|
||||
|
||||
const url = this.hass.hassUrl(this.pipelineRun!.tts!.tts_output!.url);
|
||||
const url = this.pipelineRun!.tts!.tts_output!.url;
|
||||
this._audioElement = new Audio(url);
|
||||
|
||||
this._audioElement.addEventListener("error", () => {
|
||||
|
||||
@@ -27,10 +27,7 @@ import type {
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
|
||||
import {
|
||||
findFirstActiveZone,
|
||||
getEntityLocation,
|
||||
} from "../../../../common/entity/get_entity_location";
|
||||
import { getEntityLocation } from "../../../../common/entity/get_entity_location";
|
||||
import {
|
||||
HOME_ZONE_ENTITY_ID,
|
||||
zoneColor,
|
||||
@@ -61,7 +58,6 @@ import type { EntityRegistryEntry } from "../../../../data/entity/entity_registr
|
||||
import type { HistoryStates } from "../../../../data/history";
|
||||
import { fetchDateWS } from "../../../../data/history";
|
||||
import { computeUserInitials } from "../../../../data/user";
|
||||
import { computeZoneTrackerState } from "../../../../data/zone";
|
||||
import { showMoreInfoDialog } from "../../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import type {
|
||||
CurrentUser,
|
||||
@@ -73,6 +69,7 @@ import type {
|
||||
} from "../../../../types";
|
||||
import type { ActivityEntry } from "./map-activity";
|
||||
import { personActivity, zoneActivity } from "./map-activity";
|
||||
import { computeZoneTrackerState } from "../../../../data/zone";
|
||||
|
||||
export type OverviewTab = "people" | "devices" | "zones";
|
||||
|
||||
@@ -429,8 +426,7 @@ export class HuiMapOverview extends LitElement {
|
||||
{ states: this._states, callWS: this._api.callWS },
|
||||
new Date(since),
|
||||
new Date(),
|
||||
entityIds,
|
||||
{ minimalResponse: false }
|
||||
entityIds
|
||||
);
|
||||
} catch (_err) {
|
||||
return null;
|
||||
@@ -489,7 +485,9 @@ export class HuiMapOverview extends LitElement {
|
||||
this._activity = [];
|
||||
return;
|
||||
}
|
||||
this._activity = zoneActivity(history, personIds, entityId, since);
|
||||
// A person's state is "home" for the home zone, the zone name otherwise
|
||||
const zoneState = computeZoneTrackerState(zone);
|
||||
this._activity = zoneActivity(history, personIds, zoneState, since);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -654,10 +652,7 @@ export class HuiMapOverview extends LitElement {
|
||||
? this._personName(person)
|
||||
: entry.personId
|
||||
: this._formatters.formatEntityState(stateObj, entry.state);
|
||||
const zone = entry.personId
|
||||
? undefined
|
||||
: ((entry.inZones && findFirstActiveZone(entry.inZones, this._states)) ??
|
||||
this._zoneForState(entry.state));
|
||||
const zone = entry.personId ? undefined : this._zoneForState(entry.state);
|
||||
const tile = zone
|
||||
? this._renderZoneAvatar(zone)
|
||||
: person
|
||||
|
||||
@@ -11,7 +11,6 @@ const NO_LOCATION_STATES: string[] = [UNAVAILABLE, UNKNOWN];
|
||||
export interface ActivityEntry {
|
||||
state: string;
|
||||
when: Date;
|
||||
inZones?: string[];
|
||||
/** Zone detail: the person that arrived at or left the zone */
|
||||
personId?: string;
|
||||
arrived?: boolean;
|
||||
@@ -39,22 +38,19 @@ export const personActivity = (
|
||||
if (!changed || entry.lu * 1000 < since) {
|
||||
continue;
|
||||
}
|
||||
entries.push({
|
||||
state: entry.s,
|
||||
inZones: entry.a?.in_zones,
|
||||
when: new Date(entry.lu * 1000),
|
||||
});
|
||||
entries.push({ state: entry.s, when: new Date(entry.lu * 1000) });
|
||||
}
|
||||
return entries.reverse().slice(0, ACTIVITY_MAX_ENTRIES);
|
||||
};
|
||||
|
||||
/**
|
||||
* Arrivals at and departures from a zone by the given persons, newest first.
|
||||
* A person is in the zone while their state equals zoneState.
|
||||
*/
|
||||
export const zoneActivity = (
|
||||
history: HistoryStates | undefined,
|
||||
personIds: string[],
|
||||
zoneId: string,
|
||||
zoneState: string,
|
||||
since: number
|
||||
): ActivityEntry[] => {
|
||||
const entries: ActivityEntry[] = [];
|
||||
@@ -64,7 +60,7 @@ export const zoneActivity = (
|
||||
if (NO_LOCATION_STATES.includes(entry.s)) {
|
||||
continue;
|
||||
}
|
||||
const inZone = entry.a?.in_zones?.includes(zoneId) ?? false;
|
||||
const inZone = entry.s === zoneState;
|
||||
// The first sample is an event only if it is an in-window arrival.
|
||||
const isEvent = wasInZone === undefined ? inZone : inZone !== wasInZone;
|
||||
if (isEvent && entry.lu * 1000 >= since) {
|
||||
|
||||
@@ -6,25 +6,19 @@ import { stateColorCss } from "../../../../../common/entity/state_color";
|
||||
import "../../../../../components/ha-icon";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/tile/ha-tile-badge";
|
||||
import { computeZoneTrackerState } from "../../../../../data/zone";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { RenderBadgeFunction } from "./tile-badge";
|
||||
import { computeZoneTrackerState } from "../../../../../data/zone";
|
||||
|
||||
function getZone(entity: HassEntity, hass: HomeAssistant) {
|
||||
const state = entity.state;
|
||||
if (state === "home" || state === "not_home") return undefined;
|
||||
|
||||
// Zone names aren't unique, so check the zones the entity is in first
|
||||
const zoneIds: string[] = [
|
||||
...(entity.attributes.in_zones ?? []),
|
||||
...Object.keys(hass.states).filter((entityId) =>
|
||||
entityId.startsWith("zone.")
|
||||
),
|
||||
];
|
||||
const zones = Object.values(hass.states).filter((stateObj) =>
|
||||
stateObj.entity_id.startsWith("zone.")
|
||||
);
|
||||
|
||||
return zoneIds
|
||||
.map((zoneId) => hass.states[zoneId])
|
||||
.find((zone) => zone && state === computeZoneTrackerState(zone));
|
||||
return zones.find((z) => state === computeZoneTrackerState(z));
|
||||
}
|
||||
|
||||
export const renderPersonBadge: RenderBadgeFunction = (stateObj, hass) => {
|
||||
|
||||
@@ -35,7 +35,6 @@ export class HuiBarGaugeCardFeatureEditor
|
||||
selector: {
|
||||
number: {
|
||||
mode: "box",
|
||||
step: "any",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -45,7 +44,6 @@ export class HuiBarGaugeCardFeatureEditor
|
||||
selector: {
|
||||
number: {
|
||||
mode: "box",
|
||||
step: "any",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -35,20 +35,12 @@ export class HuiNumericInputCardFeatureEditor
|
||||
selector: {
|
||||
select: {
|
||||
multiple: false,
|
||||
mode: "box",
|
||||
mode: "list",
|
||||
options: ["slider", "buttons"].map((mode) => ({
|
||||
value: mode,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.features.types.numeric-input.style_list.${mode}`
|
||||
),
|
||||
description: localize(
|
||||
`ui.panel.lovelace.editor.features.types.numeric-input.style_list.${mode}_description`
|
||||
),
|
||||
image: {
|
||||
src: `/static/images/form/feature_style_${mode}.svg`,
|
||||
src_dark: `/static/images/form/feature_style_${mode}_dark.svg`,
|
||||
flip_rtl: true,
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -116,11 +116,11 @@ export class HuiSensorCardEditor
|
||||
schema: [
|
||||
{
|
||||
name: "min",
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
selector: { number: { mode: "box" } },
|
||||
},
|
||||
{
|
||||
name: "max",
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
selector: { number: { mode: "box" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -35,20 +35,12 @@ export class HuiTargetHumidityCardFeatureEditor
|
||||
selector: {
|
||||
select: {
|
||||
multiple: false,
|
||||
mode: "box",
|
||||
mode: "list",
|
||||
options: ["slider", "buttons"].map((mode) => ({
|
||||
value: mode,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.features.types.target-humidity.style_list.${mode}`
|
||||
),
|
||||
description: localize(
|
||||
`ui.panel.lovelace.editor.features.types.target-humidity.style_list.${mode}_description`
|
||||
),
|
||||
image: {
|
||||
src: `/static/images/form/feature_style_${mode}.svg`,
|
||||
src_dark: `/static/images/form/feature_style_${mode}_dark.svg`,
|
||||
flip_rtl: true,
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -35,20 +35,12 @@ export class HuiTargetTemperatureCardFeatureEditor
|
||||
selector: {
|
||||
select: {
|
||||
multiple: false,
|
||||
mode: "box",
|
||||
mode: "list",
|
||||
options: ["buttons", "slider"].map((mode) => ({
|
||||
value: mode,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.features.types.target-temperature.style_list.${mode}`
|
||||
),
|
||||
description: localize(
|
||||
`ui.panel.lovelace.editor.features.types.target-temperature.style_list.${mode}_description`
|
||||
),
|
||||
image: {
|
||||
src: `/static/images/form/feature_style_${mode}.svg`,
|
||||
src_dark: `/static/images/form/feature_style_${mode}_dark.svg`,
|
||||
flip_rtl: true,
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,38 +1,26 @@
|
||||
import {
|
||||
mdiCellphone,
|
||||
mdiClose,
|
||||
mdiDotsVertical,
|
||||
mdiFileMoveOutline,
|
||||
mdiMonitor,
|
||||
} from "@mdi/js";
|
||||
import { mdiClose, mdiDotsVertical, mdiFileMoveOutline } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { withViewTransition } from "../../../../common/util/view-transition";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import "../../../../components/ha-tab-group";
|
||||
import "../../../../components/ha-tab-group-tab";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import {
|
||||
DEFAULT_SECTION_COLUMN_SPAN,
|
||||
isStrategySection,
|
||||
} from "../../../../data/lovelace/config/section";
|
||||
import { isStrategySection } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceStrategyConfig } from "../../../../data/lovelace/config/strategy";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import { saveConfig } from "../../../../data/lovelace/config/types";
|
||||
@@ -44,7 +32,6 @@ import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
haStyleScrollbar,
|
||||
} from "../../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import { cleanLegacyStrategyConfig } from "../../strategies/legacy-strategy";
|
||||
@@ -58,8 +45,6 @@ import {
|
||||
setAtPath,
|
||||
} from "../lovelace-path";
|
||||
import { showSelectViewDialog } from "../select-view/show-select-view-dialog";
|
||||
import "../../sections/hui-section";
|
||||
import "../../sections/hui-section-background";
|
||||
import "./hui-section-settings-editor";
|
||||
import "./hui-section-visibility-editor";
|
||||
import "./hui-section-strategy-element-editor";
|
||||
@@ -68,19 +53,10 @@ import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
import type { EditSectionDialogParams } from "./show-edit-section-dialog";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import { getViewType } from "../../views/get-view-type";
|
||||
import { DEFAULT_MAX_COLUMNS, SECTIONS_VIEW_LAYOUT } from "../../views/const";
|
||||
import { SECTIONS_VIEW_LAYOUT } from "../../views/const";
|
||||
|
||||
const TABS = ["tab-strategy", "tab-appearance", "tab-visibility"] as const;
|
||||
|
||||
type PreviewMode = "desktop" | "mobile";
|
||||
|
||||
const PREVIEW_MODES: { mode: PreviewMode; iconPath: string }[] = [
|
||||
{ mode: "mobile", iconPath: mdiCellphone },
|
||||
{ mode: "desktop", iconPath: mdiMonitor },
|
||||
];
|
||||
|
||||
const TWO_COLUMNS_MIN_WIDTH = 2 * 320 + 3 * 32;
|
||||
|
||||
@customElement("hui-dialog-edit-section")
|
||||
export class HuiDialogEditSection
|
||||
extends DirtyStateProviderMixin<LovelaceSectionRawConfig>()(LitElement)
|
||||
@@ -90,8 +66,6 @@ export class HuiDialogEditSection
|
||||
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public large = false;
|
||||
|
||||
@state() private _params?: EditSectionDialogParams;
|
||||
|
||||
@state() private _config?: LovelaceSectionRawConfig;
|
||||
@@ -108,15 +82,6 @@ export class HuiDialogEditSection
|
||||
|
||||
@state() private _yamlError = false;
|
||||
|
||||
@storage({
|
||||
key: "dashboardSectionPreviewMode",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _storedPreviewMode?: PreviewMode;
|
||||
|
||||
@state() private _defaultPreviewMode: PreviewMode = "mobile";
|
||||
|
||||
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
|
||||
|
||||
@query("hui-section-strategy-element-editor")
|
||||
@@ -152,12 +117,6 @@ export class HuiDialogEditSection
|
||||
viewPath
|
||||
);
|
||||
this._initDirtyTracking({ type: "deep" }, this._config);
|
||||
this._defaultPreviewMode =
|
||||
window.innerWidth >= TWO_COLUMNS_MIN_WIDTH ? "desktop" : "mobile";
|
||||
}
|
||||
|
||||
private get _previewMode(): PreviewMode {
|
||||
return this._storedPreviewMode ?? this._defaultPreviewMode;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -167,7 +126,6 @@ export class HuiDialogEditSection
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
this.large = false;
|
||||
this._yamlMode = false;
|
||||
this._config = undefined;
|
||||
this._currTab = TABS[0];
|
||||
@@ -181,23 +139,19 @@ export class HuiDialogEditSection
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const heading = this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_section.header"
|
||||
);
|
||||
|
||||
const strategy = isStrategySection(this._config)
|
||||
? cleanLegacyStrategyConfig(this._config.strategy)
|
||||
: undefined;
|
||||
const heading = this._computeHeading(strategy);
|
||||
const tabs = strategy ? TABS : TABS.filter((tab) => tab !== "tab-strategy");
|
||||
const currentTab =
|
||||
!strategy && this._currTab === "tab-strategy"
|
||||
? "tab-appearance"
|
||||
: this._currTab;
|
||||
|
||||
const columnSpan = Math.min(
|
||||
this._config.column_span ?? DEFAULT_SECTION_COLUMN_SPAN,
|
||||
this._viewConfig?.max_columns || DEFAULT_MAX_COLUMNS
|
||||
);
|
||||
const singleColumn = columnSpan === 1;
|
||||
const previewColumnSpan = this._previewMode === "desktop" ? columnSpan : 1;
|
||||
|
||||
let content: TemplateResult<1> | typeof nothing = nothing;
|
||||
|
||||
if (this._yamlMode) {
|
||||
@@ -247,151 +201,65 @@ export class HuiDialogEditSection
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.width=${this.large ? "full" : "large"}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@keydown=${this._ignoreKeydown}
|
||||
@closed=${this._dialogClosed}
|
||||
class=${classMap({
|
||||
"yaml-mode": this._yamlMode,
|
||||
})}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="headerNavigationIcon"
|
||||
@click=${this._cancel}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span
|
||||
slot="headerTitle"
|
||||
class="title-enlargeable"
|
||||
@click=${this._enlarge}
|
||||
>${heading}</span
|
||||
>
|
||||
<ha-dropdown
|
||||
slot="headerActionItems"
|
||||
placement="bottom-end"
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleAction}
|
||||
>
|
||||
<ha-dialog-header show-border slot="header">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="navigationIcon"
|
||||
@click=${this._cancel}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="move-to-view">
|
||||
<ha-svg-icon slot="icon" .path=${mdiFileMoveOutline}></ha-svg-icon>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_view.move_to_view"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<div class="content">
|
||||
<div class="element-editor ha-scrollbar">
|
||||
${
|
||||
!this._yamlMode
|
||||
? html`
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${tabs.map(
|
||||
(tab) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${tab}
|
||||
.active=${currentTab === tab}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.${tab.replace("-", "_")}`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
</ha-tab-group>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${this._yamlMode ? content : cache(content)}
|
||||
</div>
|
||||
<div class="element-preview ha-scrollbar">
|
||||
<div class="preview-header">
|
||||
<span id="preview-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_section.preview"
|
||||
)}
|
||||
</span>
|
||||
${
|
||||
singleColumn
|
||||
? nothing
|
||||
: html`
|
||||
<div
|
||||
class="preview-modes"
|
||||
role="group"
|
||||
aria-labelledby="preview-label"
|
||||
>
|
||||
${PREVIEW_MODES.map(
|
||||
({ mode, iconPath }) => html`
|
||||
<button
|
||||
id="preview-mode-${mode}"
|
||||
class=${classMap({
|
||||
"preview-mode": true,
|
||||
active: this._previewMode === mode,
|
||||
})}
|
||||
aria-pressed=${this._previewMode === mode}
|
||||
aria-label=${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.preview_${mode}`
|
||||
)}
|
||||
.mode=${mode}
|
||||
@click=${this._previewModeClicked}
|
||||
>
|
||||
<ha-svg-icon .path=${iconPath}></ha-svg-icon>
|
||||
</button>
|
||||
<ha-tooltip
|
||||
for="preview-mode-${mode}"
|
||||
placement="bottom"
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.preview_${mode}`
|
||||
)}
|
||||
</ha-tooltip>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
<div class="preview-frame">
|
||||
<div
|
||||
class=${classMap({
|
||||
"section-preview": true,
|
||||
wide: previewColumnSpan > 1,
|
||||
})}
|
||||
style=${styleMap({
|
||||
"--column-span": previewColumnSpan,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
class=${classMap({
|
||||
"section-container": true,
|
||||
"has-background": this._config.background !== undefined,
|
||||
blur: this._yamlError || this._strategyError,
|
||||
})}
|
||||
>
|
||||
${
|
||||
this._config.background !== undefined
|
||||
? html`
|
||||
<hui-section-background
|
||||
.hass=${this.hass}
|
||||
.background=${this._config.background}
|
||||
.theme=${this._config.theme}
|
||||
></hui-section-background>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<hui-section
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
preview
|
||||
></hui-section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span slot="title">${heading}</span>
|
||||
<ha-dropdown
|
||||
slot="actionItems"
|
||||
placement="bottom-end"
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="move-to-view">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFileMoveOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_view.move_to_view"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
${
|
||||
!this._yamlMode
|
||||
? html`
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${tabs.map(
|
||||
(tab) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${tab}
|
||||
.active=${currentTab === tab}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.${tab.replace("-", "_")}`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
</ha-tab-group>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-dialog-header>
|
||||
${this._yamlMode ? content : cache(content)}
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
@@ -450,37 +318,6 @@ export class HuiDialogEditSection
|
||||
this._updateDirtyState(this._config);
|
||||
}
|
||||
|
||||
private _computeHeading(strategy?: LovelaceStrategyConfig) {
|
||||
const header = this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_section.header"
|
||||
);
|
||||
if (!strategy) {
|
||||
return header;
|
||||
}
|
||||
const strategyName = this.hass.localize(
|
||||
`ui.panel.lovelace.editor.section_strategy.${strategy.type}.name`
|
||||
);
|
||||
if (!strategyName) {
|
||||
return header;
|
||||
}
|
||||
return this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_section.typed_header",
|
||||
{ type: strategyName }
|
||||
);
|
||||
}
|
||||
|
||||
private _previewModeClicked(ev: Event) {
|
||||
this._storedPreviewMode = (
|
||||
ev.currentTarget as HTMLElement & { mode: PreviewMode }
|
||||
).mode;
|
||||
}
|
||||
|
||||
private _enlarge() {
|
||||
withViewTransition(() => {
|
||||
this.large = !this.large;
|
||||
});
|
||||
}
|
||||
|
||||
private _handleTabChanged(ev: CustomEvent): void {
|
||||
const newTab = ev.detail.name;
|
||||
if (newTab === this._currTab) {
|
||||
@@ -689,153 +526,15 @@ export class HuiDialogEditSection
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
--code-mirror-max-height: calc(100vh - 209px);
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--dialog-content-padding: var(--ha-space-2);
|
||||
--dialog-content-padding: var(--ha-space-6);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
ha-dialog.yaml-mode {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.content {
|
||||
gap: var(--ha-space-3);
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 451px) and (min-height: 501px) {
|
||||
:host([large]) .content {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.content .preview-frame {
|
||||
padding: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.content .section-preview {
|
||||
--preview-column-width: 390px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: var(--preview-column-width);
|
||||
}
|
||||
.content .section-preview.wide {
|
||||
max-width: none;
|
||||
width: calc(
|
||||
var(--column-span) * var(--preview-column-width) +
|
||||
(var(--column-span) - 1) *
|
||||
var(--ha-view-sections-column-gap, 32px)
|
||||
);
|
||||
}
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ha-space-2);
|
||||
min-height: 32px;
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
.preview-modes {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
background: var(--ha-color-fill-neutral-normal-resting);
|
||||
}
|
||||
.preview-mode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
background: none;
|
||||
color: var(--secondary-text-color);
|
||||
cursor: pointer;
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
.preview-mode:hover {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.preview-mode.active {
|
||||
color: var(--primary-text-color);
|
||||
background: var(--card-background-color);
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / 0.2);
|
||||
}
|
||||
.preview-mode:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.content .element-editor {
|
||||
padding-inline-start: var(--ha-space-1);
|
||||
padding-inline-end: var(--ha-space-2);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
.content {
|
||||
flex-direction: row;
|
||||
max-height: var(--code-mirror-max-height);
|
||||
}
|
||||
.content > .element-editor {
|
||||
padding-inline-end: var(--ha-space-4);
|
||||
max-width: 500px;
|
||||
}
|
||||
.content > .element-editor,
|
||||
.content > .element-preview {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
height: auto;
|
||||
}
|
||||
.content .preview-frame {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
}
|
||||
.element-editor {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.blur {
|
||||
filter: blur(2px) grayscale(100%);
|
||||
}
|
||||
.element-preview {
|
||||
position: relative;
|
||||
height: max-content;
|
||||
background: var(--primary-background-color);
|
||||
padding: 4px;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
.section-container {
|
||||
position: relative;
|
||||
}
|
||||
.section-container.has-background {
|
||||
padding: var(--ha-space-2);
|
||||
border-radius: var(
|
||||
--ha-section-border-radius,
|
||||
var(--ha-border-radius-xl)
|
||||
);
|
||||
}
|
||||
.section-container hui-section {
|
||||
display: block;
|
||||
}
|
||||
ha-tab-group {
|
||||
margin-bottom: 16px;
|
||||
.gui-mode-button {
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
ha-tab-group-tab {
|
||||
flex: 1;
|
||||
@@ -844,14 +543,6 @@ export class HuiDialogEditSection
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.gui-mode-button {
|
||||
margin-right: auto;
|
||||
margin-inline-end: auto;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.title-enlargeable {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -19,71 +19,8 @@ import {
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import { DEFAULT_MAX_COLUMNS } from "../../views/const";
|
||||
|
||||
const COLUMN_SPAN_NAMES = ["single", "double", "triple"] as const;
|
||||
|
||||
const MAX_NAMED_COLUMN_SPAN = COLUMN_SPAN_NAMES.length + 1;
|
||||
|
||||
const computeColumnSpanLabel = (
|
||||
span: number,
|
||||
maxColumns: number,
|
||||
localize: LocalizeFunc
|
||||
) => {
|
||||
if (span >= maxColumns) {
|
||||
return localize(
|
||||
"ui.panel.lovelace.editor.edit_section.settings.column_span_options.full"
|
||||
);
|
||||
}
|
||||
return localize(
|
||||
`ui.panel.lovelace.editor.edit_section.settings.column_span_options.${COLUMN_SPAN_NAMES[span - 1]}`
|
||||
);
|
||||
};
|
||||
|
||||
const computeColumnSpanSchemas = (
|
||||
maxColumns: number,
|
||||
localize: LocalizeFunc
|
||||
) => {
|
||||
if (maxColumns <= 1) {
|
||||
return [];
|
||||
}
|
||||
if (maxColumns > MAX_NAMED_COLUMN_SPAN) {
|
||||
return [
|
||||
{
|
||||
name: "column_span",
|
||||
selector: {
|
||||
number: { min: 1, max: maxColumns, slider_ticks: true },
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
const options = Array.from({ length: maxColumns }, (_, i) => {
|
||||
const span = i + 1;
|
||||
return {
|
||||
value: String(span),
|
||||
label: computeColumnSpanLabel(span, maxColumns, localize),
|
||||
image: {
|
||||
src: `/static/images/form/section_column_span_${span}_of_${maxColumns}.svg`,
|
||||
src_dark: `/static/images/form/section_column_span_${span}_of_${maxColumns}_dark.svg`,
|
||||
flip_rtl: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
return [
|
||||
{
|
||||
name: "column_span",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
box_max_columns:
|
||||
maxColumns === MAX_NAMED_COLUMN_SPAN ? 2 : maxColumns,
|
||||
options,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
|
||||
interface SettingsData {
|
||||
column_span?: string | number;
|
||||
column_span?: number;
|
||||
background_enabled?: boolean;
|
||||
background_color?: string;
|
||||
background_opacity?: number;
|
||||
@@ -99,30 +36,20 @@ export class HuiDialogEditSection extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public viewConfig!: LovelaceViewConfig;
|
||||
|
||||
private get _maxColumns() {
|
||||
return this.viewConfig.max_columns || DEFAULT_MAX_COLUMNS;
|
||||
}
|
||||
|
||||
private get _displayedColumnSpan() {
|
||||
return String(
|
||||
Math.min(
|
||||
this.config.column_span ?? DEFAULT_SECTION_COLUMN_SPAN,
|
||||
this._maxColumns
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private get _columnSpanFormValue() {
|
||||
if (this._maxColumns > MAX_NAMED_COLUMN_SPAN) {
|
||||
return Number(this._displayedColumnSpan);
|
||||
}
|
||||
return this._displayedColumnSpan;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, maxColumns: number) =>
|
||||
(maxColumns: number, localize: LocalizeFunc) =>
|
||||
[
|
||||
...computeColumnSpanSchemas(maxColumns, localize),
|
||||
{
|
||||
name: "column_span",
|
||||
default: DEFAULT_SECTION_COLUMN_SPAN,
|
||||
selector: {
|
||||
number: {
|
||||
min: 1,
|
||||
max: maxColumns,
|
||||
slider_ticks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "background_enabled",
|
||||
selector: { boolean: {} },
|
||||
@@ -182,14 +109,17 @@ export class HuiDialogEditSection extends LitElement {
|
||||
const background = resolveSectionBackground(this.config.background);
|
||||
|
||||
const data: SettingsData = {
|
||||
column_span: this._columnSpanFormValue,
|
||||
column_span: this.config.column_span,
|
||||
background_enabled: backgroundEnabled,
|
||||
background_color: background?.color,
|
||||
background_opacity: background?.opacity,
|
||||
theme: this.config.theme,
|
||||
};
|
||||
|
||||
const schema = this._schema(this._localize, this._maxColumns);
|
||||
const schema = this._schema(
|
||||
this.viewConfig.max_columns || DEFAULT_MAX_COLUMNS,
|
||||
this._localize
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
@@ -224,15 +154,10 @@ export class HuiDialogEditSection extends LitElement {
|
||||
...this.config,
|
||||
};
|
||||
|
||||
if (newData.column_span === undefined) {
|
||||
if (newData.column_span) {
|
||||
newConfig.column_span = newData.column_span;
|
||||
} else {
|
||||
delete newConfig.column_span;
|
||||
} else if (String(newData.column_span) !== this._displayedColumnSpan) {
|
||||
const columnSpan = Number(newData.column_span);
|
||||
if (columnSpan === DEFAULT_SECTION_COLUMN_SPAN) {
|
||||
delete newConfig.column_span;
|
||||
} else {
|
||||
newConfig.column_span = columnSpan;
|
||||
}
|
||||
}
|
||||
|
||||
if (newData.background_enabled) {
|
||||
|
||||
@@ -3,8 +3,6 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../../../../common/dom/media_query";
|
||||
import { clamp } from "../../../../common/number/clamp";
|
||||
import { slugify } from "../../../../common/string/slugify";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
@@ -34,37 +32,6 @@ declare global {
|
||||
const VALID_PATH_REGEX = /^[a-zA-Z0-9_-]+$/;
|
||||
const INTEGER_REGEX = /^[0-9]+$/;
|
||||
|
||||
const MAX_COLUMNS_PRESETS = [1, 2, 3, 4] as const;
|
||||
const MAX_COLUMNS_MORE = "more";
|
||||
const MIN_CUSTOM_MAX_COLUMNS = MAX_COLUMNS_PRESETS.length + 1;
|
||||
const MAX_CUSTOM_MAX_COLUMNS = 10;
|
||||
|
||||
interface MaxColumnsFormData {
|
||||
max_columns: string;
|
||||
max_columns_custom?: number;
|
||||
}
|
||||
|
||||
const maxColumnsToFormData = (maxColumns: number): MaxColumnsFormData => {
|
||||
if (maxColumns <= MAX_COLUMNS_PRESETS.length) {
|
||||
return {
|
||||
max_columns: String(maxColumns),
|
||||
max_columns_custom: MIN_CUSTOM_MAX_COLUMNS,
|
||||
};
|
||||
}
|
||||
return { max_columns: MAX_COLUMNS_MORE, max_columns_custom: maxColumns };
|
||||
};
|
||||
|
||||
const maxColumnsFromFormData = (data: MaxColumnsFormData): number => {
|
||||
if (data.max_columns !== MAX_COLUMNS_MORE) {
|
||||
return Number(data.max_columns);
|
||||
}
|
||||
return clamp(
|
||||
data.max_columns_custom ?? MIN_CUSTOM_MAX_COLUMNS,
|
||||
MIN_CUSTOM_MAX_COLUMNS,
|
||||
MAX_CUSTOM_MAX_COLUMNS
|
||||
);
|
||||
};
|
||||
|
||||
@customElement("hui-view-editor")
|
||||
export class HuiViewEditor extends LitElement {
|
||||
@consumeLocalize()
|
||||
@@ -76,27 +43,10 @@ export class HuiViewEditor extends LitElement {
|
||||
|
||||
@state() private _error: Record<string, string> | undefined;
|
||||
|
||||
@state() private _narrow = false;
|
||||
|
||||
private _unsubMql?: () => void;
|
||||
|
||||
private _suggestedPath = false;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._unsubMql = listenMediaQuery("(max-width: 600px)", (matches) => {
|
||||
this._narrow = matches;
|
||||
});
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubMql?.();
|
||||
this._unsubMql = undefined;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, narrow: boolean) =>
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
{
|
||||
name: "type",
|
||||
@@ -151,33 +101,13 @@ export class HuiViewEditor extends LitElement {
|
||||
schema: [
|
||||
{
|
||||
name: "max_columns",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
box_max_columns: narrow ? 3 : 5,
|
||||
options: [
|
||||
...MAX_COLUMNS_PRESETS.map((count) => ({
|
||||
value: String(count),
|
||||
label: String(count),
|
||||
})),
|
||||
{
|
||||
value: MAX_COLUMNS_MORE,
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.edit_view.max_columns_more"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "max_columns_custom",
|
||||
visible: { field: "max_columns", value: MAX_COLUMNS_MORE },
|
||||
default: DEFAULT_MAX_COLUMNS,
|
||||
selector: {
|
||||
number: {
|
||||
min: MIN_CUSTOM_MAX_COLUMNS,
|
||||
max: MAX_CUSTOM_MAX_COLUMNS,
|
||||
mode: "box",
|
||||
min: 1,
|
||||
max: 10,
|
||||
mode: "slider",
|
||||
slider_ticks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -202,10 +132,6 @@ export class HuiViewEditor extends LitElement {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private get _maxColumns(): number {
|
||||
return this._config.max_columns ?? DEFAULT_MAX_COLUMNS;
|
||||
}
|
||||
|
||||
get _type(): string {
|
||||
return getViewType(this._config);
|
||||
}
|
||||
@@ -213,7 +139,6 @@ export class HuiViewEditor extends LitElement {
|
||||
protected render() {
|
||||
const data = {
|
||||
...this._config,
|
||||
...maxColumnsToFormData(this._maxColumns),
|
||||
type: this._type,
|
||||
theme:
|
||||
this._config.theme?.toLowerCase() === "backend-selected"
|
||||
@@ -224,7 +149,7 @@ export class HuiViewEditor extends LitElement {
|
||||
return html`
|
||||
<ha-form
|
||||
.data=${data}
|
||||
.schema=${this._schema(this._localize, this._narrow)}
|
||||
.schema=${this._schema(this._localize)}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
.computeError=${this._computeError}
|
||||
@@ -235,23 +160,7 @@ export class HuiViewEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
const {
|
||||
max_columns: maxColumnsChoice,
|
||||
max_columns_custom: maxColumnsCustom,
|
||||
...rest
|
||||
} = ev.detail.value as Omit<LovelaceViewConfig, "max_columns"> &
|
||||
MaxColumnsFormData;
|
||||
const config: LovelaceViewConfig = rest;
|
||||
|
||||
const maxColumns = maxColumnsFromFormData({
|
||||
max_columns: maxColumnsChoice,
|
||||
max_columns_custom: maxColumnsCustom,
|
||||
});
|
||||
if (maxColumns !== this._maxColumns) {
|
||||
config.max_columns = maxColumns;
|
||||
} else if (this._config.max_columns !== undefined) {
|
||||
config.max_columns = this._config.max_columns;
|
||||
}
|
||||
const config = ev.detail.value as LovelaceViewConfig;
|
||||
|
||||
if (config.type !== SECTIONS_VIEW_LAYOUT) {
|
||||
delete config.max_columns;
|
||||
@@ -302,7 +211,6 @@ export class HuiViewEditor extends LitElement {
|
||||
case "show_icon_and_title":
|
||||
case "subview":
|
||||
case "max_columns":
|
||||
case "max_columns_custom":
|
||||
case "dense_section_placement":
|
||||
case "top_margin":
|
||||
case "section_specifics":
|
||||
@@ -323,7 +231,6 @@ export class HuiViewEditor extends LitElement {
|
||||
case "path":
|
||||
case "show_icon_and_title":
|
||||
case "subview":
|
||||
case "max_columns":
|
||||
case "dense_section_placement":
|
||||
case "top_margin":
|
||||
return this._localize(
|
||||
|
||||
@@ -279,8 +279,7 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
}
|
||||
|
||||
private async _initializeConfig() {
|
||||
const config = this.config;
|
||||
let sectionConfig = { ...config };
|
||||
let sectionConfig = { ...this.config };
|
||||
let isStrategy = false;
|
||||
|
||||
if (isStrategySection(sectionConfig)) {
|
||||
@@ -289,9 +288,6 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
sectionConfig,
|
||||
this.hass!
|
||||
);
|
||||
if (config !== this.config) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
sectionConfig = {
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { STRINGS_SEPARATOR_DOT } from "../../../../common/const";
|
||||
import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||
import { getDeviceArea } from "../../../../common/entity/context/get_device_context";
|
||||
import { computeEntityEntryName } from "../../../../common/entity/compute_entity_name_display";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import {
|
||||
computeDeviceEntityCategory,
|
||||
type DeviceEntityCategory,
|
||||
} from "../../../../data/device/device_entity_category";
|
||||
import type { DeviceRegistryEntry } from "../../../../data/device/device_registry";
|
||||
import { findBatteryEntity } from "../../../../data/entity/entity_registry";
|
||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
EntitiesCardConfig,
|
||||
HeadingCardConfig,
|
||||
MarkdownCardConfig,
|
||||
TileCardConfig,
|
||||
} from "../../cards/types";
|
||||
import type { EntityConfig, SectionConfig } from "../../entity-rows/types";
|
||||
import type {
|
||||
LovelaceStrategyDependency,
|
||||
LovelaceStrategyEditor,
|
||||
} from "../types";
|
||||
|
||||
export const DEVICE_SECTION_CATEGORIES = [
|
||||
"control",
|
||||
"sensor",
|
||||
"event",
|
||||
"config",
|
||||
"diagnostic",
|
||||
] as const satisfies readonly DeviceEntityCategory[];
|
||||
|
||||
export type DeviceSectionCategory = (typeof DEVICE_SECTION_CATEGORIES)[number];
|
||||
|
||||
export const DEFAULT_DEVICE_SECTION_CATEGORIES: DeviceSectionCategory[] = [
|
||||
"control",
|
||||
"sensor",
|
||||
];
|
||||
|
||||
export const DEVICE_SECTION_HEADINGS = ["none", "device", "area"] as const;
|
||||
|
||||
export type DeviceSectionHeading = (typeof DEVICE_SECTION_HEADINGS)[number];
|
||||
|
||||
export interface DeviceSectionStrategyConfig {
|
||||
type: "device";
|
||||
device_id?: string;
|
||||
categories?: DeviceSectionCategory[];
|
||||
display?: "tiles" | "list";
|
||||
group_by_category?: boolean;
|
||||
heading?: DeviceSectionHeading;
|
||||
exclude_entities?: string[];
|
||||
}
|
||||
|
||||
const computeTileConfig = (entityId: string): TileCardConfig => ({
|
||||
type: "tile",
|
||||
entity: entityId,
|
||||
name: { type: "entity" },
|
||||
});
|
||||
|
||||
const computeRowConfig = (entityId: string): EntityConfig => ({
|
||||
entity: entityId,
|
||||
name: { type: "entity" },
|
||||
});
|
||||
|
||||
const computeDeviceSectionHeading = (
|
||||
device: DeviceRegistryEntry,
|
||||
heading: Exclude<DeviceSectionHeading, "none">,
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
localize: HomeAssistant["localize"]
|
||||
): string => {
|
||||
const area =
|
||||
heading === "area" ? getDeviceArea(device, areas, devices) : undefined;
|
||||
const parentDevice =
|
||||
device.next_name_part === "parent_device" && device.parent_device_id
|
||||
? devices[device.parent_device_id]
|
||||
: undefined;
|
||||
|
||||
const parts = [
|
||||
area ? computeAreaName(area) : undefined,
|
||||
parentDevice ? computeDeviceName(parentDevice) : undefined,
|
||||
computeDeviceName(device) ||
|
||||
localize("ui.components.device-picker.unnamed_device"),
|
||||
].filter(Boolean);
|
||||
|
||||
return parts.join(STRINGS_SEPARATOR_DOT);
|
||||
};
|
||||
|
||||
@customElement("device-section-strategy")
|
||||
export class DeviceSectionStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [
|
||||
"entities",
|
||||
"devices",
|
||||
"areas",
|
||||
];
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceStrategyEditor> {
|
||||
await import("./hui-device-section-strategy-editor");
|
||||
return document.createElement("hui-device-section-strategy-editor");
|
||||
}
|
||||
|
||||
static async generate(
|
||||
config: DeviceSectionStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceSectionConfig> {
|
||||
const section: LovelaceSectionConfig = {
|
||||
type: "grid",
|
||||
cards: [],
|
||||
};
|
||||
|
||||
const device = config.device_id
|
||||
? hass.devices[config.device_id]
|
||||
: undefined;
|
||||
|
||||
if (!device) {
|
||||
section.cards!.push({
|
||||
type: "markdown",
|
||||
content: hass.localize(
|
||||
config.device_id
|
||||
? "ui.panel.lovelace.strategy.device.device_not_found"
|
||||
: "ui.panel.lovelace.strategy.device.no_device"
|
||||
),
|
||||
} satisfies MarkdownCardConfig);
|
||||
return section;
|
||||
}
|
||||
|
||||
const heading = config.heading ?? "device";
|
||||
|
||||
const categories: readonly DeviceEntityCategory[] =
|
||||
config.categories ?? DEFAULT_DEVICE_SECTION_CATEGORIES;
|
||||
|
||||
const entries = Object.values(hass.entities)
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.device_id === device.id &&
|
||||
!entry.hidden &&
|
||||
!config.exclude_entities?.includes(entry.entity_id)
|
||||
)
|
||||
.sort((entry1, entry2) => {
|
||||
const name1 = computeEntityEntryName(entry1);
|
||||
const name2 = computeEntityEntryName(entry2);
|
||||
if (!name1 || !name2) {
|
||||
return Number(!!name1) - Number(!!name2);
|
||||
}
|
||||
return stringCompare(name1, name2, hass.locale.language);
|
||||
});
|
||||
|
||||
const batteryEntry =
|
||||
heading !== "none" ? findBatteryEntity(hass.states, entries) : undefined;
|
||||
|
||||
if (heading !== "none") {
|
||||
section.cards!.push({
|
||||
type: "heading",
|
||||
heading: computeDeviceSectionHeading(
|
||||
device,
|
||||
heading,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.localize
|
||||
),
|
||||
tap_action: hass.user?.is_admin
|
||||
? {
|
||||
action: "navigate",
|
||||
navigation_path: `/config/devices/device/${device.id}`,
|
||||
}
|
||||
: { action: "none" },
|
||||
badges: batteryEntry
|
||||
? [
|
||||
{
|
||||
type: "entity",
|
||||
entity: batteryEntry.entity_id,
|
||||
tap_action: { action: "more-info" },
|
||||
},
|
||||
]
|
||||
: [],
|
||||
} satisfies HeadingCardConfig);
|
||||
}
|
||||
|
||||
const entitiesByCategory = new Map<DeviceEntityCategory, string[]>();
|
||||
for (const entry of entries) {
|
||||
if (entry === batteryEntry) continue;
|
||||
const category = computeDeviceEntityCategory(entry);
|
||||
if (!categories.includes(category)) continue;
|
||||
if (!entitiesByCategory.has(category)) {
|
||||
entitiesByCategory.set(category, []);
|
||||
}
|
||||
entitiesByCategory.get(category)!.push(entry.entity_id);
|
||||
}
|
||||
|
||||
const shownCategories = DEVICE_SECTION_CATEGORIES.filter((category) =>
|
||||
entitiesByCategory.has(category)
|
||||
);
|
||||
const showCategoryNames =
|
||||
config.group_by_category && shownCategories.length > 1;
|
||||
const computeCategoryName = (category: DeviceSectionCategory) =>
|
||||
hass.localize(`ui.panel.lovelace.strategy.device.categories.${category}`);
|
||||
|
||||
if (config.display === "list") {
|
||||
if (shownCategories.length === 0) {
|
||||
return section;
|
||||
}
|
||||
section.cards!.push({
|
||||
type: "entities",
|
||||
entities: shownCategories.flatMap((category) => [
|
||||
...(showCategoryNames
|
||||
? [
|
||||
{
|
||||
type: "section",
|
||||
label: computeCategoryName(category),
|
||||
} satisfies SectionConfig,
|
||||
]
|
||||
: []),
|
||||
...entitiesByCategory.get(category)!.map(computeRowConfig),
|
||||
]),
|
||||
} satisfies EntitiesCardConfig);
|
||||
return section;
|
||||
}
|
||||
|
||||
for (const category of shownCategories) {
|
||||
if (showCategoryNames) {
|
||||
section.cards!.push({
|
||||
type: "heading",
|
||||
heading_style: "subtitle",
|
||||
heading: computeCategoryName(category),
|
||||
} satisfies HeadingCardConfig);
|
||||
}
|
||||
section.cards!.push(
|
||||
...entitiesByCategory.get(category)!.map(computeTileConfig)
|
||||
);
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"device-section-strategy": DeviceSectionStrategy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import { mdiFormatListBulleted } from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import type { LovelaceStrategyEditor } from "../types";
|
||||
import {
|
||||
DEFAULT_DEVICE_SECTION_CATEGORIES,
|
||||
DEVICE_SECTION_CATEGORIES,
|
||||
DEVICE_SECTION_HEADINGS,
|
||||
type DeviceSectionStrategyConfig,
|
||||
} from "./device-section-strategy";
|
||||
|
||||
@customElement("hui-device-section-strategy-editor")
|
||||
export class HuiDeviceSectionStrategyEditor
|
||||
extends LitElement
|
||||
implements LovelaceStrategyEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@state() private _config?: DeviceSectionStrategyConfig;
|
||||
|
||||
public setConfig(config: DeviceSectionStrategyConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _deviceEntities = memoizeOne(
|
||||
(deviceId: string | undefined, entities: HomeAssistant["entities"]) =>
|
||||
deviceId
|
||||
? Object.values(entities)
|
||||
.filter((entry) => entry.device_id === deviceId && !entry.hidden)
|
||||
.map((entry) => entry.entity_id)
|
||||
: []
|
||||
);
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, deviceEntities: string[]) =>
|
||||
[
|
||||
{
|
||||
name: "device_id",
|
||||
required: true,
|
||||
selector: { device: {} },
|
||||
},
|
||||
{
|
||||
name: "heading",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: DEVICE_SECTION_HEADINGS.map((heading) => ({
|
||||
value: heading,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.strategy.device.heading_options.${heading}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "categories",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
mode: "list",
|
||||
options: DEVICE_SECTION_CATEGORIES.map((category) => ({
|
||||
value: category,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.strategy.device.categories.${category}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "display",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
options: (["tiles", "list"] as const).map((display) => ({
|
||||
value: display,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.strategy.device.display_options.${display}`
|
||||
),
|
||||
description: localize(
|
||||
`ui.panel.lovelace.editor.strategy.device.display_options.${display}_description`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "group_by_category",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
...(deviceEntities.length
|
||||
? ([
|
||||
{
|
||||
name: "entities",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiFormatListBulleted,
|
||||
schema: [
|
||||
{
|
||||
name: "exclude_entities",
|
||||
selector: {
|
||||
entity: {
|
||||
multiple: true,
|
||||
include_entities: deviceEntities,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const)
|
||||
: []),
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this._config) return nothing;
|
||||
|
||||
const data = {
|
||||
categories: DEFAULT_DEVICE_SECTION_CATEGORIES,
|
||||
display: "tiles",
|
||||
heading: "device",
|
||||
...this._config,
|
||||
};
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._schema(
|
||||
this._localize,
|
||||
this._deviceEntities(
|
||||
this._config.device_id,
|
||||
this.hass?.entities ?? {}
|
||||
)
|
||||
)}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeLabel = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
this._localize(`ui.panel.lovelace.editor.strategy.device.${schema.name}`);
|
||||
|
||||
private _computeHelper = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
schema.name === "group_by_category" ||
|
||||
schema.name === "heading" ||
|
||||
schema.name === "exclude_entities"
|
||||
? this._localize(
|
||||
`ui.panel.lovelace.editor.strategy.device.${schema.name}_helper`
|
||||
)
|
||||
: "";
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<DeviceSectionStrategyConfig>) {
|
||||
ev.stopPropagation();
|
||||
const config = { ...this._config, ...ev.detail.value };
|
||||
if (config.device_id !== this._config?.device_id) {
|
||||
delete config.exclude_entities;
|
||||
}
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-device-section-strategy-editor": HuiDeviceSectionStrategyEditor;
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,7 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
|
||||
section: {
|
||||
"common-controls": () =>
|
||||
import("./usage_prediction/common-controls-section-strategy"),
|
||||
device: () => import("./device/device-section-strategy"),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ class HaChangePasswordCard extends LitElement {
|
||||
@input=${this._newPasswordChanged}
|
||||
@change=${this._newPasswordChanged}
|
||||
required
|
||||
auto-validate
|
||||
autoValidate
|
||||
></ha-input>
|
||||
<ha-input
|
||||
type="password"
|
||||
@@ -95,7 +95,7 @@ class HaChangePasswordCard extends LitElement {
|
||||
@input=${this._newPasswordConfirmChanged}
|
||||
@change=${this._newPasswordConfirmChanged}
|
||||
required
|
||||
auto-validate
|
||||
autoValidate
|
||||
></ha-input>`
|
||||
: ""
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
|
||||
return html`
|
||||
<ha-theme-settings
|
||||
.hass=${this.hass}
|
||||
.selectedTheme=${this.hass.selectedTheme}
|
||||
.narrow=${this.narrow}
|
||||
.heading=${this.hass.localize("ui.panel.profile.themes.header")}
|
||||
|
||||
@@ -121,6 +121,7 @@ class HaProfileDashboard extends LitElement {
|
||||
</ha-card>
|
||||
<ha-card outlined>
|
||||
<ha-config-navigation-list
|
||||
.hass=${this.hass}
|
||||
.pages=${pages}
|
||||
has-secondary
|
||||
.label=${this.hass.localize("panel.profile")}
|
||||
|
||||
@@ -5,10 +5,7 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { pushSupported } from "../../components/ha-push-notifications-toggle";
|
||||
import "../../components/item/ha-row-item";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
documentationUrl,
|
||||
SECURE_CONNECTION_DOCUMENTATION_PATH,
|
||||
} from "../../util/documentation-url";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
|
||||
@customElement("ha-push-notifications-row")
|
||||
class HaPushNotificationsRow extends LitElement {
|
||||
@@ -16,26 +13,14 @@ class HaPushNotificationsRow extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const platformLoaded = isComponentLoaded(this.hass.config, "html5.notify");
|
||||
let description: string;
|
||||
let documentationPath = "/integrations/html5";
|
||||
|
||||
if (!window.isSecureContext) {
|
||||
description = this.hass.localize(
|
||||
"ui.components.secure_connection_required.push_notifications"
|
||||
);
|
||||
documentationPath = SECURE_CONNECTION_DOCUMENTATION_PATH;
|
||||
} else if (!pushSupported) {
|
||||
description = this.hass.localize(
|
||||
"ui.panel.profile.push_notifications.error_not_supported"
|
||||
);
|
||||
let descriptionKey:
|
||||
"error_use_https" | "error_load_platform" | "description";
|
||||
if (!pushSupported) {
|
||||
descriptionKey = "error_use_https";
|
||||
} else if (!platformLoaded) {
|
||||
description = this.hass.localize(
|
||||
"ui.panel.profile.push_notifications.error_load_platform"
|
||||
);
|
||||
descriptionKey = "error_load_platform";
|
||||
} else {
|
||||
description = this.hass.localize(
|
||||
"ui.panel.profile.push_notifications.description"
|
||||
);
|
||||
descriptionKey = "description";
|
||||
}
|
||||
|
||||
const isDisabled = !platformLoaded || !pushSupported;
|
||||
@@ -48,9 +33,11 @@ class HaPushNotificationsRow extends LitElement {
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${description}
|
||||
>${this.hass.localize(
|
||||
`ui.panel.profile.push_notifications.${descriptionKey}`
|
||||
)}
|
||||
<a
|
||||
href=${documentationUrl(this.hass, documentationPath)}
|
||||
href=${documentationUrl(this.hass, "/integrations/html5")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
|
||||
@@ -1509,6 +1509,7 @@
|
||||
},
|
||||
"qr-scanner": {
|
||||
"select_camera": "Select camera",
|
||||
"only_https_supported": "You can only use your camera to scan a QR code when using HTTPS.",
|
||||
"not_supported": "Your browser doesn't support QR scanning.",
|
||||
"manual_input": "You can scan the QR code with another QR scanner and paste the code in the input below",
|
||||
"enter_qr_code": "Enter QR code value",
|
||||
@@ -1521,13 +1522,6 @@
|
||||
"alternativeOptionLabel": "More options"
|
||||
}
|
||||
},
|
||||
"secure_connection_required": {
|
||||
"microphone": "Your browser blocks the microphone because your connection to Home Assistant is not secured using HTTPS.",
|
||||
"camera": "Your browser blocks the camera because your connection to Home Assistant is not secured using HTTPS.",
|
||||
"push_notifications": "Your browser blocks push notifications because your connection to Home Assistant is not secured using HTTPS.",
|
||||
"learn_more": "Use the Home Assistant app, or {documentation_link} to learn how to set up a secure connection.",
|
||||
"documentation_link": "read the documentation"
|
||||
},
|
||||
"climate-control": {
|
||||
"temperature_up": "Increase temperature",
|
||||
"temperature_down": "Decrease temperature"
|
||||
@@ -1762,12 +1756,9 @@
|
||||
"send_text": "Send text",
|
||||
"start_listening": "Start listening",
|
||||
"manage_assistants": "Manage assistants",
|
||||
"microphone_permission_denied": "Home Assistant doesn't have permission to use your microphone. Allow microphone access in your browser settings and try again.",
|
||||
"microphone_not_found": "No microphone was found. Connect a microphone and try again.",
|
||||
"microphone_in_use": "Your microphone could not be started. It may be in use by another app.",
|
||||
"microphone_unknown": "Your microphone could not be started.",
|
||||
"error_playing_audio": "The response could not be played",
|
||||
"error_starting_pipeline": "Assist could not start",
|
||||
"not_supported_microphone_browser": "Your connection to Home Assistant is not secured using HTTPS. This causes browsers to block Home Assistant from accessing the microphone.",
|
||||
"not_supported_microphone_documentation": "Use the Home Assistant app or visit {documentation_link} to learn how to use a secure URL",
|
||||
"not_supported_microphone_documentation_link": "the documentation",
|
||||
"unknown_error_load_assist": "Loading the Assist pipeline failed",
|
||||
"not_found_error_load_assist": "Cannot find the Assist pipeline",
|
||||
"show_details": "Show details"
|
||||
@@ -8531,6 +8522,7 @@
|
||||
"check_logs": "Please check the logs for more information.",
|
||||
"timeout_error": "No device found after {minutes} minutes. Please check the device and try again.",
|
||||
"select_method": {
|
||||
"webcam_unsupported": "You can only use your camera to scan a QR code when using a secure connection with the app or over HTTPS.",
|
||||
"qr_code_webcam": "Scan QR code using webcam",
|
||||
"qr_code_webcam_description": "Find and scan the code on your device.",
|
||||
"qr_code_manual": "Enter QR code manually",
|
||||
@@ -9584,6 +9576,17 @@
|
||||
"not_loaded": "Usage Prediction integration is not loaded.",
|
||||
"no_data": "This place will soon fill up with the entities you use most often, based on your activity."
|
||||
},
|
||||
"device": {
|
||||
"no_device": "Select a device to show its entities.",
|
||||
"device_not_found": "This device no longer exists.",
|
||||
"categories": {
|
||||
"control": "[%key:ui::panel::config::devices::entities::control%]",
|
||||
"sensor": "[%key:ui::panel::config::devices::entities::sensor%]",
|
||||
"event": "[%key:ui::panel::config::devices::entities::event%]",
|
||||
"config": "[%key:ui::panel::config::devices::entities::config%]",
|
||||
"diagnostic": "[%key:ui::panel::config::devices::entities::diagnostic%]"
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"lights": "Lights",
|
||||
"other_lights": "Other lights",
|
||||
@@ -9972,10 +9975,7 @@
|
||||
},
|
||||
"show_icon_and_title": "Show icon and title",
|
||||
"subview": "Subview",
|
||||
"max_columns": "Max sections side by side",
|
||||
"max_columns_helper": "Smaller screens fit fewer side by side, and phones stack them one under the other.",
|
||||
"max_columns_more": "More",
|
||||
"max_columns_custom": "Number of sections",
|
||||
"max_columns": "Max number of sections wide",
|
||||
"section_specifics": "Sections view specific settings",
|
||||
"dense_section_placement": "Dense section placement",
|
||||
"dense_section_placement_helper": "Will try to fill gaps with sections that fit the gap. This may make section placement less predictable.",
|
||||
@@ -10167,17 +10167,8 @@
|
||||
"error_section": {
|
||||
"title": "Error"
|
||||
},
|
||||
"section_strategy": {
|
||||
"common-controls": {
|
||||
"name": "Common controls"
|
||||
}
|
||||
},
|
||||
"edit_section": {
|
||||
"header": "Section configuration",
|
||||
"typed_header": "{type} section configuration",
|
||||
"preview": "Preview",
|
||||
"preview_desktop": "Desktop preview",
|
||||
"preview_mobile": "Mobile preview",
|
||||
"header": "Edit section",
|
||||
"tab_strategy": "Configuration",
|
||||
"tab_visibility": "[%key:ui::panel::lovelace::editor::edit_view::tab_visibility%]",
|
||||
"tab_appearance": "Appearance",
|
||||
@@ -10185,13 +10176,7 @@
|
||||
"edit_yaml": "[%key:ui::panel::lovelace::editor::edit_view::edit_yaml%]",
|
||||
"settings": {
|
||||
"column_span": "Width",
|
||||
"column_span_helper": "Smaller screens may show it narrower, and phones stack sections one under the other.",
|
||||
"column_span_options": {
|
||||
"single": "Single",
|
||||
"double": "Double",
|
||||
"triple": "Triple",
|
||||
"full": "Full width"
|
||||
},
|
||||
"column_span_helper": "Larger sections will be made smaller to fit the display. (for example, on mobile devices)",
|
||||
"background": "Background options",
|
||||
"background_enabled": "Background",
|
||||
"background_enabled_helper": "Display a colored background behind the section",
|
||||
@@ -11460,9 +11445,7 @@
|
||||
"style": "Style",
|
||||
"style_list": {
|
||||
"buttons": "Buttons",
|
||||
"buttons_description": "Change the value step by step with minus and plus",
|
||||
"slider": "Slider",
|
||||
"slider_description": "Drag to set the value"
|
||||
"slider": "Slider"
|
||||
}
|
||||
},
|
||||
"target-temperature": {
|
||||
@@ -11470,9 +11453,7 @@
|
||||
"style": "[%key:ui::panel::lovelace::editor::features::types::numeric-input::style%]",
|
||||
"style_list": {
|
||||
"buttons": "[%key:ui::panel::lovelace::editor::features::types::numeric-input::style_list::buttons%]",
|
||||
"buttons_description": "[%key:ui::panel::lovelace::editor::features::types::numeric-input::style_list::buttons_description%]",
|
||||
"slider": "[%key:ui::panel::lovelace::editor::features::types::numeric-input::style_list::slider%]",
|
||||
"slider_description": "[%key:ui::panel::lovelace::editor::features::types::numeric-input::style_list::slider_description%]"
|
||||
"slider": "[%key:ui::panel::lovelace::editor::features::types::numeric-input::style_list::slider%]"
|
||||
}
|
||||
},
|
||||
"target-humidity": {
|
||||
@@ -11480,9 +11461,7 @@
|
||||
"style": "[%key:ui::panel::lovelace::editor::features::types::numeric-input::style%]",
|
||||
"style_list": {
|
||||
"buttons": "[%key:ui::panel::lovelace::editor::features::types::numeric-input::style_list::buttons%]",
|
||||
"buttons_description": "[%key:ui::panel::lovelace::editor::features::types::numeric-input::style_list::buttons_description%]",
|
||||
"slider": "[%key:ui::panel::lovelace::editor::features::types::numeric-input::style_list::slider%]",
|
||||
"slider_description": "[%key:ui::panel::lovelace::editor::features::types::numeric-input::style_list::slider_description%]"
|
||||
"slider": "[%key:ui::panel::lovelace::editor::features::types::numeric-input::style_list::slider%]"
|
||||
}
|
||||
},
|
||||
"water-heater-operation-modes": {
|
||||
@@ -11606,6 +11585,29 @@
|
||||
"exclude_entities_helper": "These entities will not be suggested based on your activity",
|
||||
"hide_empty": "Hide when empty"
|
||||
},
|
||||
"device": {
|
||||
"device_id": "Device",
|
||||
"categories": "Categories",
|
||||
"display": "Display",
|
||||
"display_options": {
|
||||
"tiles": "Tiles",
|
||||
"tiles_description": "One card per entity",
|
||||
"list": "List",
|
||||
"list_description": "All entities in a single card"
|
||||
},
|
||||
"group_by_category": "Group by category",
|
||||
"group_by_category_helper": "Adds a subtitle above each category",
|
||||
"heading": "Heading",
|
||||
"heading_helper": "When shown, the heading includes the battery level",
|
||||
"heading_options": {
|
||||
"none": "No heading",
|
||||
"device": "Device name",
|
||||
"area": "Area and device name"
|
||||
},
|
||||
"entities": "Entities",
|
||||
"exclude_entities": "Exclude entities",
|
||||
"exclude_entities_helper": "Not shown in this section, even if their category is selected"
|
||||
},
|
||||
"original-states": {
|
||||
"areas": "Areas to display",
|
||||
"hide_entities_without_area": "Hide entities without area",
|
||||
@@ -11800,7 +11802,7 @@
|
||||
"header": "Receive notifications",
|
||||
"description": "Enable HTML5 browser notifications on this device.",
|
||||
"error_load_platform": "Configure notify.html5.",
|
||||
"error_not_supported": "Your browser doesn't support push notifications.",
|
||||
"error_use_https": "Requires SSL enabled for frontend.",
|
||||
"push_notifications": "Push notifications",
|
||||
"link_promo": "Learn more",
|
||||
"add_device_prompt": {
|
||||
|
||||
@@ -1,21 +1,3 @@
|
||||
export type MicrophoneError =
|
||||
"permission_denied" | "not_found" | "in_use" | "unknown";
|
||||
|
||||
const microphoneErrorFromName = (name?: string): MicrophoneError => {
|
||||
switch (name) {
|
||||
case "NotAllowedError":
|
||||
case "SecurityError":
|
||||
return "permission_denied";
|
||||
case "NotFoundError":
|
||||
case "OverconstrainedError":
|
||||
return "not_found";
|
||||
case "NotReadableError":
|
||||
return "in_use";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
|
||||
export class AudioRecorder {
|
||||
private _active = false;
|
||||
|
||||
@@ -49,24 +31,20 @@ export class AudioRecorder {
|
||||
);
|
||||
}
|
||||
|
||||
public async start(): Promise<MicrophoneError | undefined> {
|
||||
public async start() {
|
||||
if (!this._context || !this._stream || !this._source || !this._recorder) {
|
||||
try {
|
||||
await this._createContext();
|
||||
} catch (err) {
|
||||
this.close();
|
||||
|
||||
return microphoneErrorFromName(
|
||||
err instanceof DOMException ? err.name : undefined
|
||||
);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._active = false;
|
||||
}
|
||||
} else {
|
||||
this._stream.getTracks()[0].enabled = true;
|
||||
await this._context.resume();
|
||||
this._active = true;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
@@ -94,7 +72,6 @@ export class AudioRecorder {
|
||||
private async _createContext() {
|
||||
// @ts-expect-error webkitAudioContext is not recognized
|
||||
const context = new (AudioContext || webkitAudioContext)();
|
||||
this._context = context;
|
||||
this._stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
// Syntax here must match an item of `parser.worker` in Webpack config in
|
||||
// order for module to be parsed and a chunk to be properly created.
|
||||
@@ -102,6 +79,7 @@ export class AudioRecorder {
|
||||
/* webpackChunkName: "recorder-worklet" */
|
||||
new URL("./recorder-worklet.js", import.meta.url)
|
||||
);
|
||||
this._context = context;
|
||||
this._source = this._context.createMediaStreamSource(this._stream);
|
||||
this._recorder = new AudioWorkletNode(this._context, "recorder-worklet");
|
||||
this._recorder.port.onmessage = (e) => {
|
||||
|
||||
@@ -4,9 +4,6 @@ const DOCUMENTATION_DOMAIN = "home-assistant.io";
|
||||
|
||||
export const DOCUMENTATION_URL = `https://www.${DOCUMENTATION_DOMAIN}`;
|
||||
|
||||
export const SECURE_CONNECTION_DOCUMENTATION_PATH =
|
||||
"/docs/configuration/securing/#remote-access";
|
||||
|
||||
export const documentationUrl = (
|
||||
{ config }: Pick<HomeAssistantConfig, "config">,
|
||||
path: string
|
||||
|
||||
@@ -106,29 +106,6 @@ describe("WaInputMixin validity", () => {
|
||||
el.handleInvalid();
|
||||
expect(el.isInvalid).toBe(true);
|
||||
});
|
||||
|
||||
it("rechecks a shown error when the value changes from outside", async () => {
|
||||
const el = createInput({ controlValid: false });
|
||||
document.body.append(el);
|
||||
el.isInvalid = true;
|
||||
await el.updateComplete;
|
||||
el.controlValid = true;
|
||||
el.value = "33";
|
||||
await el.updateComplete;
|
||||
await el.updateComplete;
|
||||
expect(el.isInvalid).toBe(false);
|
||||
el.remove();
|
||||
});
|
||||
|
||||
it("does not flag a field when the value changes from outside", async () => {
|
||||
const el = createInput({ controlValid: false });
|
||||
document.body.append(el);
|
||||
await el.updateComplete;
|
||||
el.value = "500";
|
||||
await el.updateComplete;
|
||||
expect(el.isInvalid).toBe(false);
|
||||
el.remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Polyfilled internals report validity the app cannot trust, so every path
|
||||
|
||||
@@ -8,11 +8,8 @@ import {
|
||||
|
||||
// The window starts at t=1000s; samples are (state, seconds)
|
||||
const SINCE = 1000 * 1000;
|
||||
const sample = (
|
||||
s: string,
|
||||
seconds: number,
|
||||
inZones?: string[]
|
||||
): EntityHistoryState => ({ s, lu: seconds, a: { in_zones: inZones } });
|
||||
const sample = (s: string, seconds: number): EntityHistoryState =>
|
||||
({ s, lu: seconds, a: {} }) as EntityHistoryState;
|
||||
|
||||
describe("personActivity", () => {
|
||||
it("lists state changes inside the window, newest first", () => {
|
||||
@@ -62,16 +59,13 @@ describe("zoneActivity", () => {
|
||||
{
|
||||
"person.anne": [
|
||||
sample("not_home", 900),
|
||||
sample("Work", 1100, ["zone.work"]),
|
||||
sample("Work", 1100),
|
||||
sample("not_home", 1300),
|
||||
],
|
||||
"person.bob": [
|
||||
sample("home", 900),
|
||||
sample("Work", 1200, ["zone.work"]),
|
||||
],
|
||||
"person.bob": [sample("home", 900), sample("Work", 1200)],
|
||||
},
|
||||
["person.anne", "person.bob"],
|
||||
"zone.work",
|
||||
"Work",
|
||||
SINCE
|
||||
);
|
||||
expect(
|
||||
@@ -87,13 +81,13 @@ describe("zoneActivity", () => {
|
||||
const entries = zoneActivity(
|
||||
{
|
||||
"person.anne": [
|
||||
sample("Work", 900, ["zone.work"]),
|
||||
sample("Work", 900),
|
||||
sample("home", 950),
|
||||
sample("Gym", 1100),
|
||||
],
|
||||
},
|
||||
["person.anne"],
|
||||
"zone.work",
|
||||
"Work",
|
||||
SINCE
|
||||
);
|
||||
// Leaving Work happened before the window; Gym is not Work
|
||||
@@ -104,9 +98,9 @@ describe("zoneActivity", () => {
|
||||
// A newly created person has no state at the window's start, so their
|
||||
// first sample inside the window is a real arrival
|
||||
const entries = zoneActivity(
|
||||
{ "person.anne": [sample("Work", 1100, ["zone.work"])] },
|
||||
{ "person.anne": [sample("Work", 1100)] },
|
||||
["person.anne"],
|
||||
"zone.work",
|
||||
"Work",
|
||||
SINCE
|
||||
);
|
||||
expect(entries.map((e) => [e.arrived, e.when.getTime() / 1000])).toEqual([
|
||||
@@ -118,85 +112,17 @@ describe("zoneActivity", () => {
|
||||
const entries = zoneActivity(
|
||||
{
|
||||
"person.anne": [
|
||||
sample("Work", 900, ["zone.work"]),
|
||||
sample("Work", 900),
|
||||
sample("unavailable", 1100),
|
||||
sample("Work", 1150, ["zone.work"]),
|
||||
sample("Work", 1150),
|
||||
sample("unknown", 1200),
|
||||
sample("Work", 1250, ["zone.work"]),
|
||||
sample("Work", 1250),
|
||||
],
|
||||
},
|
||||
["person.anne"],
|
||||
"zone.work",
|
||||
"Work",
|
||||
SINCE
|
||||
);
|
||||
expect(entries).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps arrivals and departures in same-named zones separate", () => {
|
||||
const history = {
|
||||
"person.anne": [
|
||||
sample("not_home", 900, []),
|
||||
sample("Work", 1100, ["zone.work"]),
|
||||
sample("not_home", 1300, []),
|
||||
],
|
||||
"person.bob": [
|
||||
sample("not_home", 900, []),
|
||||
sample("Work", 1200, ["zone.other_work"]),
|
||||
sample("not_home", 1400, []),
|
||||
],
|
||||
};
|
||||
const personIds = Object.keys(history);
|
||||
|
||||
expect(zoneActivity(history, personIds, "zone.work", SINCE)).toEqual([
|
||||
{
|
||||
state: "not_home",
|
||||
personId: "person.anne",
|
||||
arrived: false,
|
||||
when: new Date(1300 * 1000),
|
||||
},
|
||||
{
|
||||
state: "Work",
|
||||
personId: "person.anne",
|
||||
arrived: true,
|
||||
when: new Date(1100 * 1000),
|
||||
},
|
||||
]);
|
||||
expect(zoneActivity(history, personIds, "zone.other_work", SINCE)).toEqual([
|
||||
{
|
||||
state: "not_home",
|
||||
personId: "person.bob",
|
||||
arrived: false,
|
||||
when: new Date(1400 * 1000),
|
||||
},
|
||||
{
|
||||
state: "Work",
|
||||
personId: "person.bob",
|
||||
arrived: true,
|
||||
when: new Date(1200 * 1000),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("includes membership in overlapping zones without counting active-zone changes", () => {
|
||||
const entries = zoneActivity(
|
||||
{
|
||||
"person.anne": [
|
||||
sample("not_home", 900, []),
|
||||
sample("home", 1100, ["zone.home", "zone.work"]),
|
||||
sample("Work", 1200, ["zone.work"]),
|
||||
sample("not_home", 1300, []),
|
||||
],
|
||||
},
|
||||
["person.anne"],
|
||||
"zone.work",
|
||||
SINCE
|
||||
);
|
||||
|
||||
expect(
|
||||
entries.map((entry) => [entry.arrived, entry.when.getTime() / 1000])
|
||||
).toEqual([
|
||||
[false, 1300],
|
||||
[true, 1100],
|
||||
]);
|
||||
});
|
||||
});
|
||||