mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-19 12:19:32 +00:00
Compare commits
3
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}
|
||||
|
||||
@@ -109,30 +109,6 @@ const disabledFailedUnloadEntry = createConfigEntry(
|
||||
}
|
||||
);
|
||||
|
||||
// Other brands to compare how status badges look on different icons.
|
||||
const brandEntries: ConfigEntryExtended[] = (
|
||||
[
|
||||
["hue", "Philips Hue", "loaded"],
|
||||
["hue", "Philips Hue", "setup_error"],
|
||||
["mqtt", "MQTT", "setup_retry"],
|
||||
["zha", "Zigbee Home Automation", "not_loaded"],
|
||||
["sonos", "Sonos", "setup_error"],
|
||||
["shelly", "Shelly", "failed_unload"],
|
||||
["spotify", "Spotify", "setup_retry"],
|
||||
["tuya", "Tuya", "setup_error"],
|
||||
["unifi", "UniFi Network", "not_loaded"],
|
||||
["met", "Meteorologisk institutt (Met.no)", "setup_error"],
|
||||
["google_assistant", "Google Assistant", "not_loaded"],
|
||||
["homekit_controller", "HomeKit Device", "setup_retry"],
|
||||
] as [string, string, ConfigEntryExtended["state"]][]
|
||||
).map(([domain, name, entryState]) =>
|
||||
createConfigEntry(`${name} ${entryState}`, {
|
||||
domain,
|
||||
localized_domain_name: name,
|
||||
state: entryState,
|
||||
})
|
||||
);
|
||||
|
||||
const configFlows: DataEntryFlowProgressExtended[] = [
|
||||
{
|
||||
flow_id: "adbb401329d8439ebb78ef29837826a8",
|
||||
@@ -349,21 +325,6 @@ export class DemoIntegrationCard extends LitElement {
|
||||
.deviceRegistryEntries=${createDeviceRegistryEntries(loadedEntry)}
|
||||
></ha-integration-card>
|
||||
</div>
|
||||
<div class="container">
|
||||
${brandEntries.map(
|
||||
(entry) => html`
|
||||
<ha-integration-card
|
||||
.domain=${entry.domain}
|
||||
.items=${[entry]}
|
||||
.manifest=${createManifest(
|
||||
this.isCustomIntegration,
|
||||
this.isCloud
|
||||
)}
|
||||
.entityRegistryEntries=${createEntityRegistryEntries(entry)}
|
||||
></ha-integration-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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"
|
||||
|
||||
Generated
+76
-70
@@ -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 |
@@ -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;
|
||||
|
||||
+2
-5
@@ -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}`
|
||||
),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
import { mdiFileCodeOutline, mdiPackageVariant, mdiWeb } from "@mdi/js";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import {
|
||||
devicesContext,
|
||||
internationalizationContext,
|
||||
} from "../../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import type { HomeAssistantInternationalization } from "../../../types";
|
||||
import type { ConfigEntryExtended } from "./ha-config-integrations";
|
||||
|
||||
@customElement("ha-integration-card-footer")
|
||||
export class HaIntegrationCardFooter extends LitElement {
|
||||
@property({ attribute: false }) public manifest?: IntegrationManifest;
|
||||
|
||||
@property({ attribute: false }) public items!: ConfigEntryExtended[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public entityRegistryEntries!: EntityRegistryEntry[];
|
||||
|
||||
@property({ attribute: false }) public domainEntities: string[] = [];
|
||||
|
||||
@state()
|
||||
@consume({ context: devicesContext, subscribe: true })
|
||||
private _devices!: ContextType<typeof devicesContext>;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, boolean>({
|
||||
transformer: ({ language, translationMetadata }) =>
|
||||
computeRTL(language, translationMetadata.translations),
|
||||
})
|
||||
private _isRTL!: boolean;
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
const devices = this._getDevices(this.items, this._devices);
|
||||
const entitiesCount = devices.length
|
||||
? 0
|
||||
: this._getEntityCount(
|
||||
this.items,
|
||||
this.entityRegistryEntries,
|
||||
this.domainEntities
|
||||
);
|
||||
|
||||
const services = !devices.some((device) => device.entry_type !== "service");
|
||||
|
||||
const hasIcons =
|
||||
Boolean(this.manifest && !this.manifest.is_built_in) ||
|
||||
Boolean(this.manifest?.iot_class?.startsWith("cloud_")) ||
|
||||
Boolean(
|
||||
this.manifest &&
|
||||
!this.manifest.config_flow &&
|
||||
!this.items.every((itm) => itm.source === "system")
|
||||
);
|
||||
|
||||
let countBadge: TemplateResult | typeof nothing = nothing;
|
||||
if (devices.length > 0) {
|
||||
countBadge = html`<span class="count-badge"
|
||||
>${this._localize(
|
||||
`ui.panel.config.integrations.config_entry.${services ? "services" : "devices"}`,
|
||||
{ count: devices.length }
|
||||
)}</span
|
||||
>`;
|
||||
} else if (entitiesCount > 0) {
|
||||
countBadge = html`<span class="count-badge"
|
||||
>${this._localize(
|
||||
`ui.panel.config.integrations.config_entry.entities`,
|
||||
{ count: entitiesCount }
|
||||
)}</span
|
||||
>`;
|
||||
} else if (this.items.find((itm) => itm.source !== "yaml")) {
|
||||
countBadge = html`<span class="count-badge"
|
||||
>${this._localize(`ui.panel.config.integrations.config_entry.entries`, {
|
||||
count: this.items.filter((itm) => itm.source !== "yaml").length,
|
||||
})}</span
|
||||
>`;
|
||||
}
|
||||
|
||||
if (countBadge === nothing && !hasIcons) return nothing;
|
||||
|
||||
const placement = this._isRTL ? "right" : "left";
|
||||
|
||||
return html`
|
||||
<div class="footer">
|
||||
${countBadge}
|
||||
<div class="icons">
|
||||
${
|
||||
this.manifest && !this.manifest.is_built_in
|
||||
? html`<span
|
||||
class="icon ${
|
||||
this.manifest.overwrites_built_in ? "overwrites" : "custom"
|
||||
}"
|
||||
>
|
||||
<ha-svg-icon
|
||||
id="icon-custom"
|
||||
.path=${mdiPackageVariant}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="icon-custom" .placement=${placement}>
|
||||
${this._localize(
|
||||
this.manifest.overwrites_built_in
|
||||
? "ui.panel.config.integrations.config_entry.custom_overwrites_core"
|
||||
: "ui.panel.config.integrations.config_entry.custom_integration"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</span>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.manifest && this.manifest.iot_class?.startsWith("cloud_")
|
||||
? html`<span class="icon cloud">
|
||||
<ha-svg-icon id="icon-cloud" .path=${mdiWeb}></ha-svg-icon>
|
||||
<ha-tooltip for="icon-cloud" .placement=${placement}>
|
||||
${this._localize(
|
||||
"ui.panel.config.integrations.config_entry.depends_on_cloud"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</span>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.manifest &&
|
||||
!this.manifest?.config_flow &&
|
||||
!this.items.every((itm) => itm.source === "system")
|
||||
? html`<span class="icon yaml">
|
||||
<ha-svg-icon
|
||||
id="icon-yaml"
|
||||
.path=${mdiFileCodeOutline}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="icon-yaml" .placement=${placement}>
|
||||
${this._localize(
|
||||
"ui.panel.config.integrations.config_entry.no_config_flow"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</span>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getEntityCount = memoizeOne(
|
||||
(
|
||||
configEntry: ConfigEntryExtended[],
|
||||
entityRegistryEntries: EntityRegistryEntry[],
|
||||
domainEntities: string[]
|
||||
): number => {
|
||||
if (!entityRegistryEntries) {
|
||||
return domainEntities.length;
|
||||
}
|
||||
|
||||
const entryIds = configEntry
|
||||
.map((entry) => entry.entry_id)
|
||||
.filter(Boolean);
|
||||
|
||||
if (!entryIds.length) {
|
||||
return domainEntities.length;
|
||||
}
|
||||
|
||||
const entityRegEntities = entityRegistryEntries.filter(
|
||||
(entity) =>
|
||||
entity.config_entry_id && entryIds.includes(entity.config_entry_id)
|
||||
);
|
||||
|
||||
if (entityRegEntities.length === domainEntities.length) {
|
||||
return domainEntities.length;
|
||||
}
|
||||
|
||||
const entityIds = new Set<string>(
|
||||
entityRegEntities.map((reg) => reg.entity_id)
|
||||
);
|
||||
|
||||
for (const entity of domainEntities) {
|
||||
entityIds.add(entity);
|
||||
}
|
||||
|
||||
return entityIds.size;
|
||||
}
|
||||
);
|
||||
|
||||
private _getDevices = memoizeOne(
|
||||
(
|
||||
configEntry: ConfigEntryExtended[],
|
||||
deviceRegistryEntries: ContextType<typeof devicesContext>
|
||||
): DeviceRegistryEntry[] => {
|
||||
if (!deviceRegistryEntries) {
|
||||
return [];
|
||||
}
|
||||
const entryIds = configEntry.map((entry) => entry.entry_id);
|
||||
return Object.values(deviceRegistryEntries).filter((device) =>
|
||||
device.config_entries.some((entryId) => entryIds.includes(entryId))
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: var(--ha-space-2);
|
||||
min-height: var(--ha-space-8);
|
||||
}
|
||||
.count-badge {
|
||||
color: var(--ha-color-text-secondary);
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
.icons {
|
||||
display: flex;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
.icon {
|
||||
color: var(--label-badge-grey);
|
||||
padding: var(--ha-space-1);
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
}
|
||||
.icon.custom {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
.icon.overwrites {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.icon ha-svg-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-integration-card-footer": HaIntegrationCardFooter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { mdiAlertCircleOutline, mdiAlertOutline } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, 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 { transform } from "../../../common/decorators/transform";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { configContext, uiContext } from "../../../data/context";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { HomeAssistantConfig, HomeAssistantUI } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
|
||||
@customElement("ha-integration-card-header")
|
||||
export class HaIntegrationCardHeader extends LitElement {
|
||||
@property() public error?: string;
|
||||
|
||||
@property() public warning?: string;
|
||||
|
||||
@property({ attribute: false }) public localizedDomainName?: string;
|
||||
|
||||
@property() public domain!: string;
|
||||
|
||||
@property({ attribute: false }) public manifest?: IntegrationManifest;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
@transform<HomeAssistantUI, boolean | undefined>({
|
||||
transformer: ({ themes }) => themes?.darkMode,
|
||||
})
|
||||
private _darkMode?: boolean;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<HomeAssistantConfig, string>({
|
||||
transformer: ({ auth }) => auth.data.hassUrl,
|
||||
})
|
||||
private _hassUrl!: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const domainName =
|
||||
this.localizedDomainName ||
|
||||
domainToName(this._localize, this.domain, this.manifest);
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
<img
|
||||
alt=""
|
||||
src=${brandsUrl(
|
||||
{
|
||||
domain: this.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this._darkMode,
|
||||
},
|
||||
this._hassUrl
|
||||
)}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
<div class="info">
|
||||
<div
|
||||
class="primary ${this.warning || this.error ? "has-secondary" : ""}"
|
||||
role="heading"
|
||||
aria-level="1"
|
||||
>
|
||||
${domainName}
|
||||
</div>
|
||||
${
|
||||
this.error
|
||||
? html`
|
||||
<div class="secondary error">
|
||||
<ha-svg-icon .path=${mdiAlertCircleOutline}></ha-svg-icon>
|
||||
<span>${this.error}</span>
|
||||
</div>
|
||||
`
|
||||
: this.warning
|
||||
? html`
|
||||
<div class="secondary warning">
|
||||
<ha-svg-icon .path=${mdiAlertOutline}></ha-svg-icon>
|
||||
<span>${this.warning}</span>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onImageLoad(ev) {
|
||||
ev.target.style.visibility = "initial";
|
||||
}
|
||||
|
||||
private _onImageError(ev) {
|
||||
ev.target.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
flex: 1;
|
||||
padding-bottom: var(--ha-space-2);
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
direction: var(--direction);
|
||||
min-width: 0;
|
||||
}
|
||||
.header img {
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: var(--ha-space-4);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.primary {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.has-secondary {
|
||||
-webkit-line-clamp: 1;
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
.secondary {
|
||||
min-width: 0;
|
||||
--mdc-icon-size: 20px;
|
||||
font-size: var(--ha-font-size-s);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.secondary > span {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.secondary > ha-svg-icon {
|
||||
margin-inline-end: var(--ha-space-1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.error ha-svg-icon {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.warning ha-svg-icon {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-integration-card-header": HaIntegrationCardHeader;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +1,31 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiBugOutline,
|
||||
mdiFileCodeOutline,
|
||||
mdiPackageVariant,
|
||||
mdiWeb,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-ripple";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import "../../../components/item/ha-row-item";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { ERROR_STATES } from "../../../data/config_entries";
|
||||
import {
|
||||
configContext,
|
||||
devicesContext,
|
||||
uiContext,
|
||||
} from "../../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import type {
|
||||
IntegrationLogInfo,
|
||||
IntegrationManifest,
|
||||
} from "../../../data/integration";
|
||||
import { domainToName, LogSeverity } from "../../../data/integration";
|
||||
import { LogSeverity } from "../../../data/integration";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistantConfig, HomeAssistantUI } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import type { ConfigEntryExtended } from "./ha-config-integrations";
|
||||
import type { IntegrationCardSummary } from "./integration-card-summary";
|
||||
import {
|
||||
computeIntegrationCardState,
|
||||
computeIntegrationCardSummary,
|
||||
} from "./integration-card-summary";
|
||||
|
||||
interface IntegrationCardStatus {
|
||||
level: "error" | "warning" | "progress";
|
||||
/** Icon path; omitted for "progress", which shows a spinner. */
|
||||
icon?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Status shown for card states that need attention or are in progress.
|
||||
const STATE_STATUS: Partial<
|
||||
Record<ConfigEntry["state"], Omit<IntegrationCardStatus, "message">>
|
||||
> = {
|
||||
setup_error: { level: "error", icon: mdiAlertCircle },
|
||||
setup_retry: { level: "error", icon: mdiAlertCircle },
|
||||
migration_error: { level: "error", icon: mdiAlertCircle },
|
||||
not_loaded: { level: "warning", icon: mdiAlertCircle },
|
||||
failed_unload: { level: "warning", icon: mdiAlertCircle },
|
||||
setup_in_progress: { level: "progress" },
|
||||
unload_in_progress: { level: "progress" },
|
||||
};
|
||||
import "./ha-integration-card-footer";
|
||||
import "./ha-integration-card-header";
|
||||
|
||||
@customElement("ha-integration-card")
|
||||
export class HaIntegrationCard extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@property() public domain!: string;
|
||||
|
||||
@property({ attribute: false }) public items!: ConfigEntryExtended[];
|
||||
@@ -80,54 +39,22 @@ export class HaIntegrationCard extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public domainEntities: string[] = [];
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: devicesContext, subscribe: true })
|
||||
private _devices!: ContextType<typeof devicesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
@transform<HomeAssistantUI, boolean | undefined>({
|
||||
transformer: ({ themes }) => themes?.darkMode,
|
||||
})
|
||||
private _darkMode?: boolean;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<HomeAssistantConfig, string>({
|
||||
transformer: ({ auth }) => auth.data.hassUrl,
|
||||
})
|
||||
private _hassUrl!: string;
|
||||
|
||||
// There is no hover on touch devices, so a tap on a tooltip anchor shows
|
||||
// the tooltip instead of following the card link.
|
||||
private _lastPointerType?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const entryState = computeIntegrationCardState(this.items);
|
||||
const isError = ERROR_STATES.includes(entryState);
|
||||
const debugLoggingEnabled = this.logInfo?.level === LogSeverity.DEBUG;
|
||||
const entryState = this._getState(this.items);
|
||||
|
||||
const status = this._computeStatus(entryState, debugLoggingEnabled);
|
||||
|
||||
const domainName =
|
||||
this.items[0].localized_domain_name ||
|
||||
domainToName(this._localize, this.domain, this.manifest);
|
||||
const debugLoggingEnabled =
|
||||
this.logInfo && this.logInfo.level === LogSeverity.DEBUG;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
class=${classMap({
|
||||
"state-loaded": entryState === "loaded",
|
||||
"state-not-loaded": entryState === "not_loaded",
|
||||
"all-not-loaded": this.items.every(
|
||||
(entry) => entry.state === "not_loaded"
|
||||
),
|
||||
"state-failed-unload": entryState === "failed_unload",
|
||||
"state-error": isError,
|
||||
"debug-logging": debugLoggingEnabled,
|
||||
"state-setup": entryState === "setup_in_progress",
|
||||
"state-error": ERROR_STATES.includes(entryState),
|
||||
"debug-logging": Boolean(debugLoggingEnabled),
|
||||
})}
|
||||
>
|
||||
<a
|
||||
@@ -135,296 +62,55 @@ export class HaIntegrationCard extends LitElement {
|
||||
class="ripple-anchor"
|
||||
>
|
||||
<ha-ripple></ha-ripple>
|
||||
<ha-row-item>
|
||||
<img
|
||||
slot="start"
|
||||
alt=""
|
||||
src=${brandsUrl(
|
||||
{
|
||||
domain: this.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this._darkMode,
|
||||
},
|
||||
this._hassUrl
|
||||
)}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
<span slot="headline" class="name" role="heading" aria-level="1"
|
||||
>${domainName}</span
|
||||
>
|
||||
${this._renderSupportingText(status)}
|
||||
</ha-row-item>
|
||||
<div class="card-content">
|
||||
<ha-integration-card-header
|
||||
.domain=${this.domain}
|
||||
.localizedDomainName=${this.items[0].localized_domain_name}
|
||||
.error=${
|
||||
ERROR_STATES.includes(entryState)
|
||||
? this._localize(
|
||||
`ui.panel.config.integrations.config_entry.state.${entryState}`
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
.warning=${
|
||||
entryState !== "loaded" && !ERROR_STATES.includes(entryState)
|
||||
? this._localize(
|
||||
`ui.panel.config.integrations.config_entry.state.${entryState}`
|
||||
)
|
||||
: debugLoggingEnabled
|
||||
? this._localize(
|
||||
"ui.panel.config.integrations.config_entry.debug_logging_enabled"
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
.manifest=${this.manifest}
|
||||
></ha-integration-card-header>
|
||||
<ha-integration-card-footer
|
||||
.manifest=${this.manifest}
|
||||
.items=${this.items}
|
||||
.entityRegistryEntries=${this.entityRegistryEntries}
|
||||
.domainEntities=${this.domainEntities}
|
||||
></ha-integration-card-footer>
|
||||
</div>
|
||||
</a>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeStatus(
|
||||
entryState: ConfigEntry["state"],
|
||||
debugLoggingEnabled: boolean
|
||||
): IntegrationCardStatus | undefined {
|
||||
const stateStatus = STATE_STATUS[entryState];
|
||||
if (stateStatus) {
|
||||
return {
|
||||
...stateStatus,
|
||||
message: this._localize(
|
||||
`ui.panel.config.integrations.config_entry.state.${entryState}`
|
||||
),
|
||||
};
|
||||
}
|
||||
if (debugLoggingEnabled) {
|
||||
return {
|
||||
level: "warning",
|
||||
icon: mdiBugOutline,
|
||||
message: this._localize(
|
||||
"ui.panel.config.integrations.config_entry.debug_logging_enabled"
|
||||
),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _computeSupportingText(
|
||||
summary?: IntegrationCardSummary,
|
||||
status?: IntegrationCardStatus
|
||||
): string | undefined {
|
||||
if (!summary) {
|
||||
return status?.message;
|
||||
}
|
||||
if (!summary.attention) {
|
||||
return this._localize(
|
||||
`ui.panel.config.integrations.config_entry.${summary.unit}`,
|
||||
{ count: summary.count }
|
||||
);
|
||||
}
|
||||
const attentionState = summary.attentionState ?? "other";
|
||||
if (summary.attention >= summary.count) {
|
||||
return this._localize(
|
||||
`ui.panel.config.integrations.config_entry.attention.${summary.unit}_all`,
|
||||
{ count: summary.count, state: attentionState }
|
||||
);
|
||||
}
|
||||
return this._localize(
|
||||
`ui.panel.config.integrations.config_entry.attention.${summary.unit}_some`,
|
||||
{
|
||||
count: summary.count,
|
||||
attention: summary.attention,
|
||||
state: attentionState,
|
||||
private _getState = memoizeOne(
|
||||
(configEntry: ConfigEntry[]): ConfigEntry["state"] => {
|
||||
if (configEntry.length === 1) {
|
||||
return configEntry[0].state;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _renderSupportingText(
|
||||
status?: IntegrationCardStatus
|
||||
): TemplateResult | typeof nothing {
|
||||
const devices = this._getDevices(this.items, this._devices);
|
||||
const summary = computeIntegrationCardSummary(
|
||||
this.items,
|
||||
devices,
|
||||
this.entityRegistryEntries,
|
||||
devices.length
|
||||
? 0
|
||||
: this._getEntityCount(
|
||||
this.items,
|
||||
this.entityRegistryEntries,
|
||||
this.domainEntities
|
||||
)
|
||||
);
|
||||
|
||||
const icons = this._renderIcons();
|
||||
|
||||
if (!summary && !status && icons === nothing) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const text = this._computeSupportingText(summary, status);
|
||||
|
||||
return html`<span slot="supporting-text" class="supporting-text">
|
||||
${
|
||||
status
|
||||
? html`<span
|
||||
id="status"
|
||||
class="status status-${status.level}"
|
||||
@pointerdown=${this._handleTooltipPointerDown}
|
||||
@click=${this._handleTooltipClick}
|
||||
>
|
||||
${
|
||||
status.icon
|
||||
? html`<ha-svg-icon
|
||||
.path=${status.icon}
|
||||
role="img"
|
||||
aria-label=${status.message}
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-spinner
|
||||
size="tiny"
|
||||
role="img"
|
||||
aria-label=${status.message}
|
||||
></ha-spinner>`
|
||||
}
|
||||
${text ? html`<span class="text">${text}</span>` : nothing}
|
||||
</span>
|
||||
<ha-tooltip for="status" trigger="hover focus click"
|
||||
>${status.message}</ha-tooltip
|
||||
>`
|
||||
: text
|
||||
? html`<span class="text">${text}</span>`
|
||||
: nothing
|
||||
}
|
||||
${icons}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
private _renderIcons(): TemplateResult | typeof nothing {
|
||||
const manifest = this.manifest;
|
||||
if (!manifest) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const isCustom = !manifest.is_built_in;
|
||||
const isCloud = Boolean(manifest.iot_class?.startsWith("cloud_"));
|
||||
const isYaml =
|
||||
!manifest.config_flow &&
|
||||
!this.items.every((itm) => itm.source === "system");
|
||||
|
||||
if (!isCustom && !isCloud && !isYaml) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<span class="icons">
|
||||
${
|
||||
isCustom
|
||||
? this._renderIcon(
|
||||
"icon-custom",
|
||||
mdiPackageVariant,
|
||||
this._localize(
|
||||
manifest.overwrites_built_in
|
||||
? "ui.panel.config.integrations.config_entry.custom_overwrites_core"
|
||||
: "ui.panel.config.integrations.config_entry.custom_integration"
|
||||
),
|
||||
manifest.overwrites_built_in ? "overwrites" : "custom"
|
||||
)
|
||||
: nothing
|
||||
let entryState: ConfigEntry["state"];
|
||||
for (const entry of configEntry) {
|
||||
if (ERROR_STATES.includes(entry.state)) {
|
||||
return entry.state;
|
||||
}
|
||||
${
|
||||
isCloud
|
||||
? this._renderIcon(
|
||||
"icon-cloud",
|
||||
mdiWeb,
|
||||
this._localize(
|
||||
"ui.panel.config.integrations.config_entry.depends_on_cloud"
|
||||
)
|
||||
)
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
isYaml
|
||||
? this._renderIcon(
|
||||
"icon-yaml",
|
||||
mdiFileCodeOutline,
|
||||
this._localize(
|
||||
"ui.panel.config.integrations.config_entry.no_config_flow"
|
||||
)
|
||||
)
|
||||
: nothing
|
||||
}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderIcon(
|
||||
id: string,
|
||||
path: string,
|
||||
label: string,
|
||||
className = ""
|
||||
): TemplateResult {
|
||||
return html`<span
|
||||
id=${id}
|
||||
class="icon ${className}"
|
||||
role="img"
|
||||
aria-label=${label}
|
||||
@pointerdown=${this._handleTooltipPointerDown}
|
||||
@click=${this._handleTooltipClick}
|
||||
>
|
||||
<ha-svg-icon .path=${path}></ha-svg-icon>
|
||||
</span>
|
||||
<ha-tooltip for=${id} trigger="hover focus click">${label}</ha-tooltip>`;
|
||||
}
|
||||
|
||||
private _handleTooltipPointerDown(ev: PointerEvent) {
|
||||
this._lastPointerType = ev.pointerType;
|
||||
}
|
||||
|
||||
private _handleTooltipClick(ev: MouseEvent) {
|
||||
if (this._lastPointerType !== "touch") {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _onImageLoad(ev: Event) {
|
||||
(ev.target as HTMLImageElement).style.visibility = "initial";
|
||||
}
|
||||
|
||||
private _onImageError(ev: Event) {
|
||||
(ev.target as HTMLImageElement).style.visibility = "hidden";
|
||||
}
|
||||
|
||||
private _getEntityCount = memoizeOne(
|
||||
(
|
||||
configEntry: ConfigEntryExtended[],
|
||||
entityRegistryEntries: EntityRegistryEntry[],
|
||||
domainEntities: string[]
|
||||
): number => {
|
||||
if (!entityRegistryEntries) {
|
||||
return domainEntities.length;
|
||||
entryState = entry.state;
|
||||
}
|
||||
|
||||
const entryIds = configEntry
|
||||
.map((entry) => entry.entry_id)
|
||||
.filter(Boolean);
|
||||
|
||||
if (!entryIds.length) {
|
||||
return domainEntities.length;
|
||||
}
|
||||
|
||||
const entityRegEntities = entityRegistryEntries.filter(
|
||||
(entity) =>
|
||||
entity.config_entry_id && entryIds.includes(entity.config_entry_id)
|
||||
);
|
||||
|
||||
if (entityRegEntities.length === domainEntities.length) {
|
||||
return domainEntities.length;
|
||||
}
|
||||
|
||||
const entityIds = new Set<string>(
|
||||
entityRegEntities.map((reg) => reg.entity_id)
|
||||
);
|
||||
|
||||
for (const entity of domainEntities) {
|
||||
entityIds.add(entity);
|
||||
}
|
||||
|
||||
return entityIds.size;
|
||||
}
|
||||
);
|
||||
|
||||
private _getDevices = memoizeOne(
|
||||
(
|
||||
configEntry: ConfigEntryExtended[],
|
||||
deviceRegistryEntries: ContextType<typeof devicesContext>
|
||||
): DeviceRegistryEntry[] => {
|
||||
if (!deviceRegistryEntries) {
|
||||
return [];
|
||||
}
|
||||
const entryIds = configEntry.map((entry) => entry.entry_id);
|
||||
return Object.values(deviceRegistryEntries).filter((device) =>
|
||||
device.config_entries.some((entryId) => entryIds.includes(entryId))
|
||||
);
|
||||
return entryState!;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -439,6 +125,7 @@ export class HaIntegrationCard extends LitElement {
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
--state-color: var(--divider-color, #e0e0e0);
|
||||
--state-message-color: var(--state-color);
|
||||
}
|
||||
ha-card:hover {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
@@ -461,107 +148,41 @@ export class HaIntegrationCard extends LitElement {
|
||||
background-color: var(--secondary-text-color);
|
||||
opacity: 0.08;
|
||||
}
|
||||
ha-row-item {
|
||||
.card-content {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
--ha-row-item-padding-block: var(--ha-space-4);
|
||||
--ha-row-item-padding-inline: var(--ha-space-4);
|
||||
--ha-row-item-gap: var(--ha-space-4);
|
||||
}
|
||||
/* Fill the card height so content stays centered when a neighboring
|
||||
card in the grid row is taller */
|
||||
ha-row-item::part(base) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
ha-row-item::part(headline) {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
.name {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.supporting-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-1);
|
||||
min-height: calc(
|
||||
var(--ha-font-size-s) * var(--ha-line-height-normal)
|
||||
);
|
||||
}
|
||||
.supporting-text .text,
|
||||
.status .text {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-1);
|
||||
min-width: 0;
|
||||
line-height: 16px;
|
||||
}
|
||||
.status ha-svg-icon,
|
||||
.status ha-spinner {
|
||||
flex-shrink: 0;
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
.status ha-spinner {
|
||||
--track-width: 2px;
|
||||
}
|
||||
.status-error ha-svg-icon {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.status-warning ha-svg-icon {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
.icons {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: var(--ha-space-1);
|
||||
margin-inline-start: auto;
|
||||
padding-inline-start: var(--ha-space-1);
|
||||
}
|
||||
.icon {
|
||||
display: flex;
|
||||
color: var(--label-badge-grey);
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
.icon.custom {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
.icon.overwrites {
|
||||
color: var(--error-color);
|
||||
flex-direction: column;
|
||||
}
|
||||
.debug-logging {
|
||||
--state-color: var(--warning-color);
|
||||
--ha-card-border-color: var(--state-color);
|
||||
--text-on-state-color: var(--primary-text-color);
|
||||
}
|
||||
.state-error {
|
||||
--state-color: var(--error-color);
|
||||
--ha-card-border-color: var(--state-color);
|
||||
--text-on-state-color: var(--text-primary-color);
|
||||
}
|
||||
.state-failed-unload {
|
||||
--state-color: var(--warning-color);
|
||||
--ha-card-border-color: var(--state-color);
|
||||
--text-on-state-color: var(--primary-text-color);
|
||||
}
|
||||
.state-not-loaded {
|
||||
opacity: 0.8;
|
||||
--state-color: var(--warning-color);
|
||||
--ha-card-border-color: var(--state-color);
|
||||
--state-message-color: var(--primary-text-color);
|
||||
}
|
||||
.all-not-loaded {
|
||||
.state-setup {
|
||||
opacity: 0.8;
|
||||
--state-message-color: var(--secondary-text-color);
|
||||
}
|
||||
:host(.highlight) ha-card {
|
||||
--state-color: var(--primary-color);
|
||||
--ha-card-border-color: var(--state-color);
|
||||
--text-on-state-color: var(--text-primary-color);
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { ERROR_STATES } from "../../../data/config_entries";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
|
||||
export type IntegrationCardSummaryUnit =
|
||||
"devices" | "services" | "entities" | "entries";
|
||||
|
||||
export interface IntegrationCardSummary {
|
||||
unit: IntegrationCardSummaryUnit;
|
||||
/** Total number of items of `unit`. */
|
||||
count: number;
|
||||
/** Number of items of `unit` that belong to a config entry needing attention. */
|
||||
attention: number;
|
||||
/**
|
||||
* The state shared by all config entries needing attention, or `undefined`
|
||||
* when they are in different states.
|
||||
*/
|
||||
attentionState?: ConfigEntry["state"];
|
||||
}
|
||||
|
||||
// Transient states (setup/unload in progress) and "loaded" don't need attention.
|
||||
const ATTENTION_STATES: ConfigEntry["state"][] = [
|
||||
...ERROR_STATES,
|
||||
"not_loaded",
|
||||
"failed_unload",
|
||||
];
|
||||
|
||||
// Card state priority, most important first.
|
||||
const CARD_STATE_PRIORITY: ConfigEntry["state"][] = [
|
||||
...ATTENTION_STATES,
|
||||
"setup_in_progress",
|
||||
"unload_in_progress",
|
||||
];
|
||||
|
||||
export const configEntryNeedsAttention = (entry: ConfigEntry): boolean =>
|
||||
ATTENTION_STATES.includes(entry.state);
|
||||
|
||||
/**
|
||||
* The state the integration card shows for a group of config entries: the
|
||||
* most important state of any entry, so the card never looks healthy while
|
||||
* one of its entries needs attention.
|
||||
*/
|
||||
export const computeIntegrationCardState = (
|
||||
entries: ConfigEntry[]
|
||||
): ConfigEntry["state"] => {
|
||||
for (const cardState of CARD_STATE_PRIORITY) {
|
||||
if (entries.some((entry) => entry.state === cardState)) {
|
||||
return cardState;
|
||||
}
|
||||
}
|
||||
return "loaded";
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute what the integration card shows as supporting text: the number of
|
||||
* devices, services, entities or entries, and how many of those belong to a
|
||||
* config entry that needs attention.
|
||||
*
|
||||
* When an entry needing attention owns none of the counted items (for
|
||||
* example an entry that never set up and has no devices yet), the summary
|
||||
* counts entries instead so the problem is never hidden.
|
||||
*/
|
||||
export const computeIntegrationCardSummary = (
|
||||
entries: ConfigEntry[],
|
||||
devices: DeviceRegistryEntry[],
|
||||
entityRegistryEntries: EntityRegistryEntry[],
|
||||
entityCount: number
|
||||
): IntegrationCardSummary | undefined => {
|
||||
const attentionEntries = entries.filter(configEntryNeedsAttention);
|
||||
const attentionEntryIds = new Set(
|
||||
attentionEntries.map((entry) => entry.entry_id)
|
||||
);
|
||||
const attentionStates = new Set(attentionEntries.map((entry) => entry.state));
|
||||
const attentionState =
|
||||
attentionStates.size === 1 ? [...attentionStates][0] : undefined;
|
||||
const uiEntries = entries.filter((entry) => entry.source !== "yaml");
|
||||
|
||||
// Attention entries that own at least one of the counted items.
|
||||
const representedEntryIds = new Set<string>();
|
||||
let summary: IntegrationCardSummary | undefined;
|
||||
|
||||
if (devices.length) {
|
||||
let attention = 0;
|
||||
for (const device of devices) {
|
||||
const deviceAttentionIds = device.config_entries.filter((id) =>
|
||||
attentionEntryIds.has(id)
|
||||
);
|
||||
if (deviceAttentionIds.length) {
|
||||
attention++;
|
||||
deviceAttentionIds.forEach((id) => representedEntryIds.add(id));
|
||||
}
|
||||
}
|
||||
summary = {
|
||||
unit: devices.every((device) => device.entry_type === "service")
|
||||
? "services"
|
||||
: "devices",
|
||||
count: devices.length,
|
||||
attention,
|
||||
};
|
||||
} else if (entityCount > 0) {
|
||||
let attention = 0;
|
||||
for (const entity of entityRegistryEntries) {
|
||||
if (
|
||||
entity.config_entry_id &&
|
||||
attentionEntryIds.has(entity.config_entry_id)
|
||||
) {
|
||||
attention++;
|
||||
representedEntryIds.add(entity.config_entry_id);
|
||||
}
|
||||
}
|
||||
summary = {
|
||||
unit: "entities",
|
||||
count: entityCount,
|
||||
attention: Math.min(entityCount, attention),
|
||||
};
|
||||
} else if (uiEntries.length) {
|
||||
return {
|
||||
unit: "entries",
|
||||
count: uiEntries.length,
|
||||
attention: attentionEntryIds.size,
|
||||
attentionState,
|
||||
};
|
||||
}
|
||||
|
||||
if (representedEntryIds.size < attentionEntryIds.size) {
|
||||
return {
|
||||
unit: "entries",
|
||||
count: Math.max(uiEntries.length, attentionEntryIds.size),
|
||||
attention: attentionEntryIds.size,
|
||||
attentionState,
|
||||
};
|
||||
}
|
||||
|
||||
return summary && { ...summary, attentionState };
|
||||
};
|
||||
+6
-4
@@ -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" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+1
-9
@@ -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,
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
|
||||
+1
-9
@@ -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,
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
+43
-33
@@ -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"
|
||||
@@ -7528,16 +7519,6 @@
|
||||
"entities": "{count} {count, plural,\n one {entity}\n other {entities}\n}",
|
||||
"services": "{count} {count, plural,\n one {service}\n other {services}\n}",
|
||||
"entries": "{count} {count, plural,\n one {entry}\n other {entries}\n}",
|
||||
"attention": {
|
||||
"devices_some": "{attention} of {count} {count, plural,\n one {device}\n other {devices}\n} {state, select,\n setup_error {failed to set up}\n setup_retry {failed to set up}\n migration_error {failed to migrate}\n failed_unload {failed to unload}\n not_loaded {{attention, plural,\n one {is}\n other {are}\n } not loaded}\n other {{attention, plural,\n one {needs}\n other {need}\n } attention}\n}",
|
||||
"devices_all": "{count} {count, plural,\n one {device}\n other {devices}\n} {state, select,\n setup_error {failed to set up}\n setup_retry {failed to set up}\n migration_error {failed to migrate}\n failed_unload {failed to unload}\n not_loaded {{count, plural,\n one {is}\n other {are}\n } not loaded}\n other {{count, plural,\n one {needs}\n other {need}\n } attention}\n}",
|
||||
"services_some": "{attention} of {count} {count, plural,\n one {service}\n other {services}\n} {state, select,\n setup_error {failed to set up}\n setup_retry {failed to set up}\n migration_error {failed to migrate}\n failed_unload {failed to unload}\n not_loaded {{attention, plural,\n one {is}\n other {are}\n } not loaded}\n other {{attention, plural,\n one {needs}\n other {need}\n } attention}\n}",
|
||||
"services_all": "{count} {count, plural,\n one {service}\n other {services}\n} {state, select,\n setup_error {failed to set up}\n setup_retry {failed to set up}\n migration_error {failed to migrate}\n failed_unload {failed to unload}\n not_loaded {{count, plural,\n one {is}\n other {are}\n } not loaded}\n other {{count, plural,\n one {needs}\n other {need}\n } attention}\n}",
|
||||
"entities_some": "{attention} of {count} {count, plural,\n one {entity}\n other {entities}\n} {state, select,\n setup_error {failed to set up}\n setup_retry {failed to set up}\n migration_error {failed to migrate}\n failed_unload {failed to unload}\n not_loaded {{attention, plural,\n one {is}\n other {are}\n } not loaded}\n other {{attention, plural,\n one {needs}\n other {need}\n } attention}\n}",
|
||||
"entities_all": "{count} {count, plural,\n one {entity}\n other {entities}\n} {state, select,\n setup_error {failed to set up}\n setup_retry {failed to set up}\n migration_error {failed to migrate}\n failed_unload {failed to unload}\n not_loaded {{count, plural,\n one {is}\n other {are}\n } not loaded}\n other {{count, plural,\n one {needs}\n other {need}\n } attention}\n}",
|
||||
"entries_some": "{attention} of {count} {count, plural,\n one {entry}\n other {entries}\n} {state, select,\n setup_error {failed to set up}\n setup_retry {failed to set up}\n migration_error {failed to migrate}\n failed_unload {failed to unload}\n not_loaded {{attention, plural,\n one {is}\n other {are}\n } not loaded}\n other {{attention, plural,\n one {needs}\n other {need}\n } attention}\n}",
|
||||
"entries_all": "{count} {count, plural,\n one {entry}\n other {entries}\n} {state, select,\n setup_error {failed to set up}\n setup_retry {failed to set up}\n migration_error {failed to migrate}\n failed_unload {failed to unload}\n not_loaded {{count, plural,\n one {is}\n other {are}\n } not loaded}\n other {{count, plural,\n one {needs}\n other {need}\n } attention}\n}"
|
||||
},
|
||||
"no_devices_or_entities": "No devices or entities",
|
||||
"devices_without_subentry": "Devices that don't belong to a sub-entry",
|
||||
"copy": "Copy entry ID",
|
||||
@@ -8541,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",
|
||||
@@ -9594,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",
|
||||
@@ -11452,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": {
|
||||
@@ -11462,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": {
|
||||
@@ -11472,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": {
|
||||
@@ -11598,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",
|
||||
@@ -11792,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
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ConfigEntry } from "../../../../src/data/config_entries";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../../src/data/entity/entity_registry";
|
||||
import {
|
||||
computeIntegrationCardState,
|
||||
computeIntegrationCardSummary,
|
||||
} from "../../../../src/panels/config/integrations/integration-card-summary";
|
||||
|
||||
const entry = (
|
||||
entry_id: string,
|
||||
state: ConfigEntry["state"] = "loaded",
|
||||
source = "user"
|
||||
) => ({ entry_id, state, source }) as ConfigEntry;
|
||||
|
||||
const device = (
|
||||
config_entries: string[],
|
||||
entry_type: DeviceRegistryEntry["entry_type"] = null
|
||||
) => ({ config_entries, entry_type }) as DeviceRegistryEntry;
|
||||
|
||||
const entity = (config_entry_id: string) =>
|
||||
({ config_entry_id }) as EntityRegistryEntry;
|
||||
|
||||
describe("computeIntegrationCardState", () => {
|
||||
it("returns the most important state regardless of entry order", () => {
|
||||
expect(
|
||||
computeIntegrationCardState([entry("a", "not_loaded"), entry("b")])
|
||||
).toBe("not_loaded");
|
||||
expect(
|
||||
computeIntegrationCardState([
|
||||
entry("a", "setup_in_progress"),
|
||||
entry("b", "setup_retry"),
|
||||
])
|
||||
).toBe("setup_retry");
|
||||
expect(computeIntegrationCardState([entry("a"), entry("b")])).toBe(
|
||||
"loaded"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeIntegrationCardSummary", () => {
|
||||
it("counts devices, services and entities that need attention", () => {
|
||||
expect(
|
||||
computeIntegrationCardSummary(
|
||||
[entry("a"), entry("b", "setup_error")],
|
||||
[device(["a"]), device(["b"])],
|
||||
[],
|
||||
0
|
||||
)
|
||||
).toEqual({
|
||||
unit: "devices",
|
||||
count: 2,
|
||||
attention: 1,
|
||||
attentionState: "setup_error",
|
||||
});
|
||||
|
||||
expect(
|
||||
computeIntegrationCardSummary(
|
||||
[entry("a", "setup_retry")],
|
||||
[device(["a"], "service")],
|
||||
[],
|
||||
0
|
||||
)
|
||||
).toEqual({
|
||||
unit: "services",
|
||||
count: 1,
|
||||
attention: 1,
|
||||
attentionState: "setup_retry",
|
||||
});
|
||||
|
||||
expect(
|
||||
computeIntegrationCardSummary(
|
||||
[entry("a"), entry("b", "not_loaded")],
|
||||
[],
|
||||
[entity("a"), entity("b"), entity("b")],
|
||||
3
|
||||
)
|
||||
).toEqual({
|
||||
unit: "entities",
|
||||
count: 3,
|
||||
attention: 2,
|
||||
attentionState: "not_loaded",
|
||||
});
|
||||
});
|
||||
|
||||
it("has no shared attention state when entries fail differently", () => {
|
||||
expect(
|
||||
computeIntegrationCardSummary(
|
||||
[entry("a", "setup_error"), entry("b", "not_loaded")],
|
||||
[device(["a"]), device(["b"])],
|
||||
[],
|
||||
0
|
||||
)?.attentionState
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("counts entries when a failing entry owns none of the counted items", () => {
|
||||
// "c" failed before creating any device, so counting devices would hide it.
|
||||
expect(
|
||||
computeIntegrationCardSummary(
|
||||
[entry("a"), entry("b", "setup_error"), entry("c", "setup_error")],
|
||||
[device(["a"]), device(["b"])],
|
||||
[],
|
||||
0
|
||||
)
|
||||
).toEqual({
|
||||
unit: "entries",
|
||||
count: 3,
|
||||
attention: 2,
|
||||
attentionState: "setup_error",
|
||||
});
|
||||
|
||||
expect(
|
||||
computeIntegrationCardSummary(
|
||||
[entry("a"), entry("b", "setup_retry")],
|
||||
[],
|
||||
[entity("a")],
|
||||
1
|
||||
)
|
||||
).toEqual({
|
||||
unit: "entries",
|
||||
count: 2,
|
||||
attention: 1,
|
||||
attentionState: "setup_retry",
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores YAML entries when counting entries", () => {
|
||||
expect(
|
||||
computeIntegrationCardSummary(
|
||||
[entry("a", "loaded", "yaml"), entry("b")],
|
||||
[],
|
||||
[],
|
||||
0
|
||||
)
|
||||
).toEqual({ unit: "entries", count: 1, attention: 0 });
|
||||
expect(
|
||||
computeIntegrationCardSummary([entry("a", "loaded", "yaml")], [], [], 0)
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -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],
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user