mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-28 20:17:18 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 854e56947f |
@@ -41,14 +41,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -62,4 +62,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
|
||||
+248
-248
File diff suppressed because one or more lines are too long
+1
-1
@@ -13,4 +13,4 @@ nodeLinker: node-modules
|
||||
|
||||
npmMinimalAgeGate: 3d
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.15.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.14.1.cjs
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@material/mwc-top-app-bar-fixed";
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../src/components/ha-icon-button";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@material/mwc-top-app-bar-fixed";
|
||||
import { mdiMenu, mdiSwapHorizontal } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
@@ -10,7 +11,6 @@ import type { HaDrawer } from "../../src/components/ha-drawer";
|
||||
import { HaExpansionPanel } from "../../src/components/ha-expansion-panel";
|
||||
import "../../src/components/ha-icon-button";
|
||||
import "../../src/components/ha-svg-icon";
|
||||
import "../../src/components/ha-top-app-bar-fixed";
|
||||
import "../../src/managers/notification-manager";
|
||||
import { haStyle } from "../../src/resources/styles";
|
||||
import { PAGES, SIDEBAR } from "../build/import-pages";
|
||||
@@ -84,7 +84,7 @@ class HaGallery extends LitElement {
|
||||
<div class="drawer-title">Home Assistant Design</div>
|
||||
<div class="sidebar">${sidebar}</div>
|
||||
<div slot="appContent" class="app-content">
|
||||
<ha-top-app-bar-fixed>
|
||||
<mwc-top-app-bar-fixed>
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@click=${this._menuTapped}
|
||||
@@ -94,7 +94,7 @@ class HaGallery extends LitElement {
|
||||
<div slot="title">
|
||||
${PAGES[this._page].metadata.title || this._page.split("/")[1]}
|
||||
</div>
|
||||
</ha-top-app-bar-fixed>
|
||||
</mwc-top-app-bar-fixed>
|
||||
<div class="content">
|
||||
${PAGES[this._page].description
|
||||
? html`
|
||||
@@ -227,12 +227,11 @@ class HaGallery extends LitElement {
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
--ha-sidebar-width: 256px;
|
||||
--header-height: 64px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
box-sizing: border-box;
|
||||
max-height: calc(100vh - var(--header-height));
|
||||
max-height: calc(100vh - 64px);
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
@@ -244,7 +243,7 @@ class HaGallery extends LitElement {
|
||||
display: flex;
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
min-height: var(--header-height);
|
||||
min-height: 64px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
@@ -278,10 +277,6 @@ class HaGallery extends LitElement {
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
|
||||
ha-drawer[type="dismissible"][open] ha-top-app-bar-fixed {
|
||||
--ha-top-app-bar-width: calc(100% - var(--ha-sidebar-width));
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { provide } from "@lit/context";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-card";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../../../src/components/ha-tip";
|
||||
import { internationalizationContext } from "../../../../src/data/context";
|
||||
import type { HomeAssistantInternationalization } from "../../../../src/types";
|
||||
import "../../../../src/components/ha-card";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
|
||||
const tips: (string | TemplateResult)[] = [
|
||||
"Test tip",
|
||||
@@ -16,25 +14,16 @@ const tips: (string | TemplateResult)[] = [
|
||||
|
||||
@customElement("demo-components-ha-tip")
|
||||
export class DemoHaTip extends LitElement {
|
||||
@provide({ context: internationalizationContext })
|
||||
@state()
|
||||
protected _i18n: HomeAssistantInternationalization = {
|
||||
localize: ((key: string) => key) as any,
|
||||
language: "en",
|
||||
selectedLanguage: null,
|
||||
locale: {} as any,
|
||||
translationMetadata: {} as any,
|
||||
loadBackendTranslation: (async () => (key: string) => key) as any,
|
||||
loadFragmentTranslation: (async () => (key: string) => key) as any,
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html` ${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-tip ${mode} demo">
|
||||
<div class="card-content">
|
||||
${tips.map((tip) => html`<ha-tip>${tip}</ha-tip>`)}
|
||||
${tips.map(
|
||||
(tip) =>
|
||||
html`<ha-tip .hass=${provideHass(this)}>${tip}</ha-tip>`
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
[build.environment]
|
||||
YARN_VERSION = "1.22.11"
|
||||
NODE_OPTIONS = "--max_old_space_size=6144"
|
||||
|
||||
+33
-29
@@ -38,41 +38,45 @@
|
||||
"@codemirror/search": "6.7.0",
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@codemirror/view": "6.43.0",
|
||||
"@date-fns/tz": "1.5.0",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.4.6",
|
||||
"@formatjs/intl-displaynames": "7.3.8",
|
||||
"@formatjs/intl-durationformat": "0.10.12",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.9",
|
||||
"@formatjs/intl-listformat": "8.3.8",
|
||||
"@formatjs/intl-locale": "5.3.8",
|
||||
"@formatjs/intl-numberformat": "9.3.9",
|
||||
"@formatjs/intl-pluralrules": "6.3.8",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.8",
|
||||
"@formatjs/intl-datetimeformat": "7.4.5",
|
||||
"@formatjs/intl-displaynames": "7.3.7",
|
||||
"@formatjs/intl-durationformat": "0.10.11",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.8",
|
||||
"@formatjs/intl-listformat": "8.3.7",
|
||||
"@formatjs/intl-locale": "5.3.7",
|
||||
"@formatjs/intl-numberformat": "9.3.8",
|
||||
"@formatjs/intl-pluralrules": "6.3.7",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.7",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@home-assistant/webawesome": "3.7.0-ha.0",
|
||||
"@home-assistant/webawesome": "3.3.1-ha.3",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
"@lit-labs/virtualizer": "2.1.1",
|
||||
"@lit/context": "1.1.6",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/mwc-base": "0.27.0",
|
||||
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "2.4.1",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.21",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "4.0.5",
|
||||
"@tsparticles/preset-links": "4.0.5",
|
||||
"@tsparticles/engine": "4.0.0",
|
||||
"@tsparticles/preset-links": "4.0.0",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
@@ -83,19 +87,19 @@
|
||||
"core-js": "3.49.0",
|
||||
"cropperjs": "1.6.2",
|
||||
"culori": "4.0.2",
|
||||
"date-fns": "4.3.0",
|
||||
"date-fns": "4.1.0",
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"echarts": "6.1.0",
|
||||
"echarts": "6.0.0",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.3.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "7.0.0",
|
||||
"hls.js": "1.6.16",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.4",
|
||||
"intl-messageformat": "11.2.7",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.2.6",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -103,7 +107,7 @@
|
||||
"lit": "3.3.3",
|
||||
"lit-html": "3.3.3",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "18.0.4",
|
||||
"marked": "18.0.3",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -115,7 +119,7 @@
|
||||
"sortablejs": "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "4.0.0",
|
||||
"tinykeys": "3.0.0",
|
||||
"weekstart": "2.0.0",
|
||||
"workbox-cacheable-response": "7.4.1",
|
||||
"workbox-core": "7.4.1",
|
||||
@@ -132,13 +136,13 @@
|
||||
"@babel/preset-env": "7.29.5",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.1",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.61.0",
|
||||
"@html-eslint/eslint-plugin": "0.60.0",
|
||||
"@lokalise/node-api": "16.0.0",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.11",
|
||||
"@rspack/core": "2.0.4",
|
||||
"@rspack/core": "2.0.3",
|
||||
"@rspack/dev-server": "2.0.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
@@ -157,12 +161,12 @@
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.1.7",
|
||||
"@vitest/coverage-v8": "4.1.6",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.4.0",
|
||||
"eslint": "10.3.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-plugin-import-x": "4.16.2",
|
||||
@@ -172,7 +176,7 @@
|
||||
"eslint-plugin-wc": "3.1.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.5",
|
||||
"generate-license-file": "4.2.1",
|
||||
"generate-license-file": "4.1.1",
|
||||
"glob": "13.0.6",
|
||||
"globals": "17.6.0",
|
||||
"gulp": "5.0.1",
|
||||
@@ -184,7 +188,7 @@
|
||||
"jsdom": "29.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"license-checker-rseidelsohn": "4.4.2",
|
||||
"lint-staged": "17.0.5",
|
||||
"lint-staged": "17.0.4",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.18.1",
|
||||
@@ -198,9 +202,9 @@
|
||||
"terser-webpack-plugin": "5.6.0",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.59.4",
|
||||
"typescript-eslint": "8.59.3",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.7",
|
||||
"vitest": "4.1.6",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.4.1#~/.yarn/patches/workbox-build-npm-7.4.1-c84561662c.patch"
|
||||
@@ -216,8 +220,8 @@
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
},
|
||||
"packageManager": "yarn@4.15.0",
|
||||
"packageManager": "yarn@4.14.1",
|
||||
"volta": {
|
||||
"node": "24.16.0"
|
||||
"node": "24.15.0"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260527.0"
|
||||
version = "20260429.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -18,46 +18,7 @@
|
||||
"enabled": true,
|
||||
"schedule": ["on the 19th day of the month before 4am"]
|
||||
},
|
||||
"customDatasources": {
|
||||
"ha-core-python": {
|
||||
"defaultRegistryUrlTemplate": "https://raw.githubusercontent.com/home-assistant/core/dev/.python-version",
|
||||
"format": "plain"
|
||||
}
|
||||
},
|
||||
"customManagers": [
|
||||
{
|
||||
"description": "Keep PYTHON_VERSION in sync with home-assistant/core (patch + minor)",
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/workflows/[^/]+\\.ya?ml$/"],
|
||||
"matchStrings": ["PYTHON_VERSION: \"(?<currentValue>[^\"]+)\""],
|
||||
"depNameTemplate": "python",
|
||||
"datasourceTemplate": "custom.ha-core-python",
|
||||
"versioningTemplate": "python"
|
||||
},
|
||||
{
|
||||
"description": "Keep devcontainer image and requires-python in sync with home-assistant/core (minor only)",
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": [
|
||||
"/^\\.devcontainer/Dockerfile$/",
|
||||
"/^pyproject\\.toml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"devcontainers/python:(?<currentValue>[\\d.]+)",
|
||||
"requires-python = \">=(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "python",
|
||||
"datasourceTemplate": "custom.ha-core-python",
|
||||
"versioningTemplate": "python",
|
||||
"extractVersionTemplate": "^(?<version>\\d+\\.\\d+)"
|
||||
}
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Group all Python version updates from home-assistant/core",
|
||||
"matchDepNames": ["python"],
|
||||
"matchDatasources": ["custom.ha-core-python"],
|
||||
"groupName": "Python version"
|
||||
},
|
||||
{
|
||||
"description": "MDC packages are pinned to the same version as MWC",
|
||||
"extends": ["monorepo:material-components-web"],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @param arr - The array to get combinations of
|
||||
* @returns A multidimensional array of all possible combinations
|
||||
*/
|
||||
export function getAllCombinations<T>(arr: readonly T[]): T[][] {
|
||||
export function getAllCombinations<T>(arr: T[]) {
|
||||
return arr.reduce<T[][]>(
|
||||
(combinations, element) =>
|
||||
combinations.concat(
|
||||
|
||||
@@ -114,15 +114,6 @@ export const DOMAINS_WITH_DYNAMIC_PICTURE = new Set([
|
||||
export const UNIT_C = "°C";
|
||||
export const UNIT_F = "°F";
|
||||
|
||||
/** Length units. */
|
||||
export const UNIT_IN = "in";
|
||||
export const UNIT_KM = "km";
|
||||
export const UNIT_MM = "mm";
|
||||
|
||||
/** Pressure units. */
|
||||
export const UNIT_HPA = "hPa";
|
||||
export const UNIT_INHG = "inHg";
|
||||
|
||||
/** Entity ID of the default view. */
|
||||
export const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
|
||||
|
||||
|
||||
@@ -1,20 +1,6 @@
|
||||
import timezones from "google-timezones-json";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
|
||||
const RESOLVED_RAW = Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
|
||||
|
||||
// Some environments (e.g. Android emulator) return a UTC offset like "+00:00"
|
||||
// instead of an IANA zone name. Only accept values that are known IANA zones,
|
||||
// matching the list used by ha-timezone-picker.
|
||||
const RESOLVED_TIME_ZONE =
|
||||
RESOLVED_RAW &&
|
||||
(RESOLVED_RAW === "UTC" ||
|
||||
RESOLVED_RAW === "Etc/UTC" ||
|
||||
RESOLVED_RAW in timezones)
|
||||
? RESOLVED_RAW
|
||||
: undefined;
|
||||
|
||||
export const HAS_RESOLVED_IANA_TIME_ZONE = RESOLVED_TIME_ZONE !== undefined;
|
||||
const RESOLVED_TIME_ZONE = Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
|
||||
|
||||
// Browser time zone can be determined from Intl, with fallback to UTC for polyfill or no support.
|
||||
export const LOCAL_TIME_ZONE = RESOLVED_TIME_ZONE ?? "UTC";
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../types";
|
||||
import {
|
||||
entitiesContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../../data/context";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { entitiesContext, statesContext } from "../../data/context";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import type { LocalizeFunc } from "../translations/localize";
|
||||
import { transform } from "./transform";
|
||||
|
||||
interface ConsumeEntryConfig {
|
||||
@@ -99,15 +91,3 @@ export const consumeEntityRegistryEntry = (config: ConsumeEntryConfig) =>
|
||||
return typeof id === "string" ? entities?.[id] : undefined;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Consumes `internationalizationContext` and narrows it to the `localize`
|
||||
* function. No host watching is needed — the decorated property updates
|
||||
* whenever the i18n context changes.
|
||||
*/
|
||||
export const consumeLocalize = () =>
|
||||
composeDecorator<HomeAssistantInternationalization, LocalizeFunc>(
|
||||
internationalizationContext,
|
||||
undefined,
|
||||
({ localize }) => localize
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import { isUnavailableState } from "../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
interface EntityUnitStubConfig {
|
||||
@@ -21,24 +21,32 @@ export const computeEntityUnitDisplay = (
|
||||
stateObj: HassEntity | undefined,
|
||||
config: EntityUnitStubConfig
|
||||
): string => {
|
||||
let unit;
|
||||
if (
|
||||
!stateObj ||
|
||||
stateObj.state === UNAVAILABLE ||
|
||||
stateObj.state === UNKNOWN ||
|
||||
(!config.attribute && stateObj.attributes.device_class === "duration")
|
||||
stateObj &&
|
||||
!isUnavailableState(stateObj.state) &&
|
||||
(config.attribute || stateObj.attributes.device_class !== "duration")
|
||||
) {
|
||||
return "";
|
||||
// check for an explicitly defined unit in config
|
||||
unit = config.unit;
|
||||
|
||||
if (!unit) {
|
||||
if (!config.attribute) {
|
||||
// use entity's unit_of_measurement
|
||||
const stateParts = hass.formatEntityStateToParts(stateObj);
|
||||
unit = stateParts.find((part) => part.type === "unit")?.value;
|
||||
} else {
|
||||
// use attribute's unit if available
|
||||
const attrParts = hass.formatEntityAttributeValueToParts(
|
||||
stateObj,
|
||||
config.attribute
|
||||
);
|
||||
unit = attrParts.find((part) => part.type === "unit")?.value;
|
||||
}
|
||||
}
|
||||
|
||||
return unit ?? "";
|
||||
}
|
||||
|
||||
// check for an explicitly defined unit in config
|
||||
if (config.unit) {
|
||||
return config.unit;
|
||||
}
|
||||
|
||||
// otherwise derive from the entity's state or attribute
|
||||
const parts = config.attribute
|
||||
? hass.formatEntityAttributeValueToParts(stateObj, config.attribute)
|
||||
: hass.formatEntityStateToParts(stateObj);
|
||||
|
||||
return parts.find((part) => part.type === "unit")?.value ?? "";
|
||||
return "";
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import { UNAVAILABLE_STATES } from "../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { stringCompare } from "../string/compare";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
@@ -253,7 +253,7 @@ export const getStatesDomain = (
|
||||
|
||||
if (!attribute) {
|
||||
// All entities can have unavailable states
|
||||
result.push(UNAVAILABLE, UNKNOWN);
|
||||
result.push(...UNAVAILABLE_STATES);
|
||||
}
|
||||
|
||||
if (!attribute && domain in FIXED_DOMAIN_STATES) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
|
||||
@@ -8,18 +8,12 @@ export const computeGroupEntitiesState = (states: HassEntity[]): string => {
|
||||
return UNAVAILABLE;
|
||||
}
|
||||
|
||||
const allUnavailable = states.every(
|
||||
(stateObj) => stateObj.state === UNAVAILABLE
|
||||
const validState = states.some(
|
||||
(stateObj) => !isUnavailableState(stateObj.state)
|
||||
);
|
||||
if (allUnavailable) {
|
||||
return UNAVAILABLE;
|
||||
}
|
||||
|
||||
const hasValidState = states.some(
|
||||
(stateObj) => stateObj.state !== UNAVAILABLE && stateObj.state !== UNKNOWN
|
||||
);
|
||||
if (!hasValidState) {
|
||||
return UNKNOWN;
|
||||
if (!validState) {
|
||||
return UNAVAILABLE;
|
||||
}
|
||||
|
||||
// Use the first state to determine the domain
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { OFF, UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import { isUnavailableState, OFF, UNAVAILABLE } from "../../data/entity/entity";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
|
||||
export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||
@@ -19,7 +19,7 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||
return compareState !== UNAVAILABLE;
|
||||
}
|
||||
|
||||
if (compareState === UNAVAILABLE || compareState === UNKNOWN) {
|
||||
if (isUnavailableState(compareState)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* @summary Truncates a string to `maxLength`, appending `ellipsis` only when it actually shortens the result.
|
||||
* @param text The input string.
|
||||
* @param maxLength Maximum length of the prefix kept before the ellipsis.
|
||||
* @param ellipsis Suffix appended when truncation occurs.
|
||||
* @returns `text` unchanged when its length is `<= maxLength + ellipsis.length`, otherwise `text.substring(0, maxLength) + ellipsis`.
|
||||
*/
|
||||
export const truncateWithEllipsis = (
|
||||
text: string,
|
||||
maxLength: number,
|
||||
ellipsis = "..."
|
||||
): string => {
|
||||
if (text.length <= maxLength + ellipsis.length) {
|
||||
return text;
|
||||
}
|
||||
return `${text.substring(0, maxLength)}${ellipsis}`;
|
||||
};
|
||||
@@ -9,7 +9,7 @@ export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
*
|
||||
* @summary
|
||||
* Small status indicator dot used in automation/condition rows to surface the
|
||||
* live evaluation result.
|
||||
* live evaluation result. Renders an optional tooltip with details on hover.
|
||||
*
|
||||
* @attr {"pass"|"fail"|"invalid"|"unknown"} state - The current live-test state. Defaults to `unknown`.
|
||||
* @attr {string} label - Accessible label announced by assistive technology.
|
||||
@@ -56,15 +56,31 @@ export class HaAutomationRowLiveTest extends LitElement {
|
||||
background-color: var(--ha-color-fill-success-loud-resting);
|
||||
border-color: var(--ha-color-fill-success-loud-resting);
|
||||
}
|
||||
:host([state="pass"]) #indicator:hover {
|
||||
background-color: var(--ha-color-fill-success-loud-hover);
|
||||
border-color: var(--ha-color-fill-success-loud-hover);
|
||||
}
|
||||
:host([state="fail"]) #indicator {
|
||||
border-color: var(--ha-color-fill-warning-loud-resting);
|
||||
}
|
||||
:host([state="fail"]) #indicator:hover {
|
||||
background-color: var(--ha-color-fill-warning-loud-hover);
|
||||
border-color: var(--ha-color-fill-warning-loud-hover);
|
||||
}
|
||||
:host([state="invalid"]) #indicator {
|
||||
border-color: var(--ha-color-fill-danger-loud-resting);
|
||||
}
|
||||
:host([state="invalid"]) #indicator:hover {
|
||||
background-color: var(--ha-color-fill-danger-loud-hover);
|
||||
border-color: var(--ha-color-fill-danger-loud-hover);
|
||||
}
|
||||
:host([state="unknown"]) #indicator {
|
||||
border-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
}
|
||||
:host([state="unknown"]) #indicator:hover {
|
||||
background-color: var(--ha-color-fill-neutral-loud-hover);
|
||||
border-color: var(--ha-color-fill-neutral-loud-hover);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,9 +128,7 @@ export class HaAutomationRow extends LitElement {
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
padding-left: var(--ha-space-3);
|
||||
padding-inline-start: var(--ha-space-3);
|
||||
padding-inline-end: initial;
|
||||
padding: 0 0 0 var(--ha-space-3);
|
||||
min-height: 48px;
|
||||
align-items: flex-start;
|
||||
cursor: pointer;
|
||||
@@ -146,8 +144,6 @@ export class HaAutomationRow extends LitElement {
|
||||
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
margin-left: calc(var(--ha-space-2) * -1);
|
||||
margin-inline-start: calc(var(--ha-space-2) * -1);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
:host([building-block]) .leading-icon-wrapper {
|
||||
background-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
@@ -191,6 +187,7 @@ export class HaAutomationRow extends LitElement {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
margin: 0 var(--ha-space-3);
|
||||
}
|
||||
::slotted([slot="header"]) {
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
@@ -116,7 +116,7 @@ export class HaProgressButton extends LitElement {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
:host([appearance="brand"]) ha-svg-icon {
|
||||
ha-svg-icon {
|
||||
color: var(--white-color);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -956,11 +956,29 @@ export class HaChartBase extends LitElement {
|
||||
const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
|
||||
| XAXisOption
|
||||
| undefined;
|
||||
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
|
||||
| YAXisOption
|
||||
| undefined;
|
||||
const series = ensureArray(this.data).map((s) => {
|
||||
const data = this._hiddenDatasets.has(String(s.id ?? s.name))
|
||||
? undefined
|
||||
: s.data;
|
||||
if (data && s.type === "line") {
|
||||
if (yAxis?.type === "log") {
|
||||
// set <=0 values to null so they render as gaps on a log graph
|
||||
return {
|
||||
...s,
|
||||
data: (data as LineSeriesOption["data"])!.map((v) =>
|
||||
Array.isArray(v)
|
||||
? [
|
||||
v[0],
|
||||
typeof v[1] !== "number" || v[1] > 0 ? v[1] : null,
|
||||
...v.slice(2),
|
||||
]
|
||||
: v
|
||||
),
|
||||
};
|
||||
}
|
||||
if (s.sampling === "minmax") {
|
||||
const minX = xAxis?.min
|
||||
? xAxis.min instanceof Date
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { mdiDragHorizontalVariant, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
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 { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button";
|
||||
import "../ha-dialog-footer";
|
||||
import "../ha-icon-button";
|
||||
@@ -25,9 +24,7 @@ import type { DataTableSettingsDialogParams } from "./show-dialog-data-table-set
|
||||
|
||||
@customElement("dialog-data-table-settings")
|
||||
export class DialogDataTableSettings extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: DataTableSettingsDialogParams;
|
||||
|
||||
@@ -120,7 +117,7 @@ export class DialogDataTableSettings extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const localize = this._params.localizeFunc || this._localize;
|
||||
const localize = this._params.localizeFunc || this.hass.localize;
|
||||
|
||||
const columns = this._sortedColumns(
|
||||
this._params.columns,
|
||||
@@ -175,7 +172,7 @@ export class DialogDataTableSettings extends LitElement {
|
||||
.hidden=${!isVisible}
|
||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||
slot="meta"
|
||||
.label=${localize(
|
||||
.label=${this.hass!.localize(
|
||||
`ui.components.data-table.settings.${isVisible ? "hide" : "show"}`,
|
||||
{ title: typeof col.title === "string" ? col.title : "" }
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import type { DeviceAutomation } from "../../data/device/device_automation";
|
||||
import {
|
||||
@@ -14,7 +12,7 @@ import {
|
||||
sortDeviceAutomations,
|
||||
} from "../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { CallWS, HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
|
||||
@@ -48,14 +46,13 @@ export abstract class HaDeviceAutomationPicker<
|
||||
}
|
||||
|
||||
private _localizeDeviceAutomation: (
|
||||
localize: LocalizeFunc,
|
||||
states: HassEntities,
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
automation: T
|
||||
) => string;
|
||||
|
||||
private _fetchDeviceAutomations: (
|
||||
callWS: CallWS,
|
||||
hass: HomeAssistant,
|
||||
deviceId: string
|
||||
) => Promise<T[]>;
|
||||
|
||||
@@ -130,8 +127,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
|
||||
const automationListItems = automations.map((automation, idx) => {
|
||||
const primary = this._localizeDeviceAutomation(
|
||||
this.hass.localize,
|
||||
this.hass.states,
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
automation
|
||||
);
|
||||
@@ -166,12 +162,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
);
|
||||
|
||||
const text = automation
|
||||
? this._localizeDeviceAutomation(
|
||||
this.hass.localize,
|
||||
this.hass.states,
|
||||
this._entityReg,
|
||||
automation
|
||||
)
|
||||
? this._localizeDeviceAutomation(this.hass, this._entityReg, automation)
|
||||
: value === NO_AUTOMATION_KEY
|
||||
? this.NO_AUTOMATION_TEXT
|
||||
: value;
|
||||
@@ -181,9 +172,9 @@ export abstract class HaDeviceAutomationPicker<
|
||||
|
||||
private async _updateDeviceInfo() {
|
||||
this._automations = this.deviceId
|
||||
? (
|
||||
await this._fetchDeviceAutomations(this.hass.callWS, this.deviceId)
|
||||
).sort(sortDeviceAutomations)
|
||||
? (await this._fetchDeviceAutomations(this.hass, this.deviceId)).sort(
|
||||
sortDeviceAutomations
|
||||
)
|
||||
: // No device, clear the list of automations
|
||||
[];
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { STATES_OFF } from "../../common/const";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import {
|
||||
UNAVAILABLE,
|
||||
UNKNOWN,
|
||||
isUnavailableState,
|
||||
} from "../../data/entity/entity";
|
||||
import { forwardHaptic } from "../../data/haptics";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-formfield";
|
||||
@@ -16,8 +20,7 @@ import "../ha-switch";
|
||||
const isOn = (stateObj?: HassEntity) =>
|
||||
stateObj !== undefined &&
|
||||
!STATES_OFF.includes(stateObj.state) &&
|
||||
stateObj.state !== UNAVAILABLE &&
|
||||
stateObj.state !== UNKNOWN;
|
||||
!isUnavailableState(stateObj.state);
|
||||
|
||||
/**
|
||||
* @element ha-entity-toggle
|
||||
|
||||
@@ -9,7 +9,7 @@ import secondsToDuration from "../../common/datetime/seconds_to_duration";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import { timerTimeRemaining } from "../../data/timer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -170,8 +170,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
}
|
||||
// eslint-disable-next-line: disable=no-fallthrough
|
||||
default:
|
||||
return entityState.state === UNAVAILABLE ||
|
||||
entityState.state === UNKNOWN
|
||||
return isUnavailableState(entityState.state)
|
||||
? "—"
|
||||
: this.hass!.formatEntityStateToParts(entityState).find(
|
||||
(part) => part.type === "value"
|
||||
@@ -210,7 +209,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
_timerTimeRemaining = 0
|
||||
) {
|
||||
// For unavailable states or certain domains, use a special translation that is truncated to fit within the badge label
|
||||
if (entityState.state === UNAVAILABLE || entityState.state === UNKNOWN) {
|
||||
if (isUnavailableState(entityState.state)) {
|
||||
return this.hass!.localize(`state_badge.default.${entityState.state}`);
|
||||
}
|
||||
const domainStateKey = getTruncatedKey(domain, entityState.state);
|
||||
|
||||
@@ -6,9 +6,8 @@ import {
|
||||
mdiInformationOutline,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-icon-button";
|
||||
@@ -40,9 +39,7 @@ class HaAlert extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public dismissable = false;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize?: LocalizeFunc;
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@@ -71,7 +68,7 @@ class HaAlert extends LitElement {
|
||||
${this.dismissable
|
||||
? html`<ha-icon-button
|
||||
@click=${this._dismissClicked}
|
||||
.label=${this._localize?.("ui.common.dismiss_alert")}
|
||||
.label=${this.localize!("ui.common.dismiss_alert")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface AreaControlPickerItem extends PickerComboBoxItem {
|
||||
deviceClass?: string;
|
||||
}
|
||||
|
||||
const AREA_CONTROL_DOMAINS = [
|
||||
const AREA_CONTROL_DOMAINS: readonly AreaControlDomain[] = [
|
||||
"light",
|
||||
"fan",
|
||||
"switch",
|
||||
@@ -43,7 +43,7 @@ const AREA_CONTROL_DOMAINS = [
|
||||
"cover-door",
|
||||
"cover-window",
|
||||
"cover-damper",
|
||||
] as const satisfies readonly AreaControlDomain[];
|
||||
] as const;
|
||||
|
||||
@customElement("ha-area-controls-picker")
|
||||
export class HaAreaControlsPicker extends LitElement {
|
||||
@@ -130,7 +130,7 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
(excludeValues !== undefined && excludeValues.includes(id));
|
||||
|
||||
const controlEntities = getAreaControlEntities(
|
||||
AREA_CONTROL_DOMAINS,
|
||||
AREA_CONTROL_DOMAINS as unknown as AreaControlDomain[],
|
||||
areaId,
|
||||
excludeEntities,
|
||||
this.hass
|
||||
|
||||
@@ -61,6 +61,7 @@ export class HaAreasDisplayEditor extends LitElement {
|
||||
>
|
||||
<ha-svg-icon slot="leading-icon" .path=${mdiTextureBox}></ha-svg-icon>
|
||||
<ha-items-display-editor
|
||||
.hass=${this.hass}
|
||||
.items=${items}
|
||||
.value=${value}
|
||||
@value-changed=${this._areaDisplayChanged}
|
||||
|
||||
@@ -107,6 +107,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
<ha-items-display-editor
|
||||
.hass=${this.hass}
|
||||
.items=${groupedAreasItems[floor.floor_id]}
|
||||
.value=${value}
|
||||
.floorId=${floor.floor_id}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { ClimateEntity } from "../data/climate";
|
||||
import { CLIMATE_PRESET_NONE } from "../data/climate";
|
||||
import { OFF, UNAVAILABLE, UNKNOWN } from "../data/entity/entity";
|
||||
import { isUnavailableState, OFF } from "../data/entity/entity";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-climate-state")
|
||||
@@ -14,11 +14,9 @@ class HaClimateState extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const currentStatus = this._computeCurrentStatus();
|
||||
const noValue =
|
||||
this.stateObj.state === UNAVAILABLE || this.stateObj.state === UNKNOWN;
|
||||
|
||||
return html`<div class="target">
|
||||
${!noValue
|
||||
${!isUnavailableState(this.stateObj.state)
|
||||
? html`<span class="state-label">
|
||||
${this._localizeState()}
|
||||
${this.stateObj.attributes.preset_mode &&
|
||||
@@ -34,7 +32,7 @@ class HaClimateState extends LitElement {
|
||||
: this._localizeState()}
|
||||
</div>
|
||||
|
||||
${currentStatus && !noValue
|
||||
${currentStatus && !isUnavailableState(this.stateObj.state)
|
||||
? html`
|
||||
<div class="current">
|
||||
${this.hass.localize("ui.card.climate.currently")}:
|
||||
@@ -121,10 +119,7 @@ class HaClimateState extends LitElement {
|
||||
}
|
||||
|
||||
private _localizeState(): string {
|
||||
if (
|
||||
this.stateObj.state === UNAVAILABLE ||
|
||||
this.stateObj.state === UNKNOWN
|
||||
) {
|
||||
if (isUnavailableState(this.stateObj.state)) {
|
||||
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -294,7 +294,6 @@ export class HaDrawer extends LitElement {
|
||||
border-inline-end: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
|
||||
box-sizing: border-box;
|
||||
transition: width var(--ha-animation-duration-normal) ease;
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
|
||||
@@ -6,18 +6,11 @@ import type { HaIconButton } from "./ha-icon-button";
|
||||
|
||||
/**
|
||||
* Event type for the ha-dropdown component when an item is selected.
|
||||
* @param TValue - The type of the selected item's `value`.
|
||||
* @param TData - The type of the selected item's `data` when set on `ha-dropdown-item`.
|
||||
* @param T - The type of the value of the selected item.
|
||||
*/
|
||||
export type HaDropdownSelectEvent<TValue = string, TData = undefined> = [
|
||||
TData,
|
||||
] extends [undefined]
|
||||
? CustomEvent<{
|
||||
item: Omit<HaDropdownItem, "value"> & { value: TValue };
|
||||
}>
|
||||
: CustomEvent<{
|
||||
item: Omit<HaDropdownItem, "value"> & { value: TValue; data: TData };
|
||||
}>;
|
||||
export type HaDropdownSelectEvent<T = string> = CustomEvent<{
|
||||
item: Omit<HaDropdownItem, "value"> & { value: T };
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Home Assistant dropdown component
|
||||
|
||||
@@ -54,6 +54,7 @@ export class HaEntitiesDisplayEditor extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-items-display-editor
|
||||
.hass=${this.hass}
|
||||
.items=${items}
|
||||
.value=${value}
|
||||
@value-changed=${this._itemDisplayChanged}
|
||||
|
||||
@@ -109,8 +109,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
@property({ attribute: "custom-value-label" })
|
||||
public customValueLabel?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-sort" }) public noSort = false;
|
||||
|
||||
@query(".container") private _containerElement?: HTMLDivElement;
|
||||
|
||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
@@ -273,7 +271,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
.selectedSection=${this.selectedSection}
|
||||
.searchKeys=${this.searchKeys}
|
||||
.customValueLabel=${this.customValueLabel}
|
||||
.noSort=${this.noSort}
|
||||
></ha-picker-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,77 +1,59 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
// @ts-ignore
|
||||
import topAppBarStyles from "@material/top-app-bar/dist/mdc.top-app-bar.min.css";
|
||||
import { css, html, LitElement, unsafeCSS } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-header-bar")
|
||||
export class HaHeaderBar extends LitElement {
|
||||
protected render() {
|
||||
return html`<header class="header-bar">
|
||||
<div class="row">
|
||||
<section class="section" id="navigation">
|
||||
return html`<header class="mdc-top-app-bar">
|
||||
<div class="mdc-top-app-bar__row">
|
||||
<section
|
||||
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-start"
|
||||
id="navigation"
|
||||
>
|
||||
<slot name="navigationIcon"></slot>
|
||||
<span class="title">
|
||||
<span class="mdc-top-app-bar__title">
|
||||
<slot name="title"></slot>
|
||||
</span>
|
||||
</section>
|
||||
<section class="section end" id="actions" role="toolbar">
|
||||
<section
|
||||
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-end"
|
||||
id="actions"
|
||||
role="toolbar"
|
||||
>
|
||||
<slot name="actionItems"></slot>
|
||||
</section>
|
||||
</div>
|
||||
</header>`;
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
box-sizing: border-box;
|
||||
color: var(--app-header-text-color, var(--primary-text-color));
|
||||
background-color: var(
|
||||
--app-header-background-color,
|
||||
var(--primary-background-color)
|
||||
);
|
||||
padding: var(--header-bar-padding);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: var(--header-height);
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
padding: 0 var(--ha-space-3);
|
||||
}
|
||||
|
||||
#navigation {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.section.end {
|
||||
flex: none;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: var(--header-height);
|
||||
padding-inline-start: var(--ha-space-6);
|
||||
}
|
||||
`;
|
||||
static get styles() {
|
||||
return [
|
||||
unsafeCSS(topAppBarStyles),
|
||||
css`
|
||||
.mdc-top-app-bar__row {
|
||||
height: var(--header-height);
|
||||
}
|
||||
.mdc-top-app-bar {
|
||||
position: static;
|
||||
color: var(--mdc-theme-on-primary, #fff);
|
||||
padding: var(--header-bar-padding);
|
||||
}
|
||||
.mdc-top-app-bar__section.mdc-top-app-bar__section--align-start {
|
||||
flex: 1;
|
||||
}
|
||||
.mdc-top-app-bar__section.mdc-top-app-bar__section--align-end {
|
||||
flex: none;
|
||||
}
|
||||
.mdc-top-app-bar__title {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
padding-inline-start: 24px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { OFF, UNAVAILABLE, UNKNOWN } from "../data/entity/entity";
|
||||
import { isUnavailableState, OFF } from "../data/entity/entity";
|
||||
import type { HumidifierEntity } from "../data/humidifier";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@@ -13,11 +13,9 @@ class HaHumidifierState extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const currentStatus = this._computeCurrentStatus();
|
||||
const noValue =
|
||||
this.stateObj.state === UNAVAILABLE || this.stateObj.state === UNKNOWN;
|
||||
|
||||
return html`<div class="target">
|
||||
${!noValue
|
||||
${!isUnavailableState(this.stateObj.state)
|
||||
? html`<span class="state-label">
|
||||
${this._localizeState()}
|
||||
${this.stateObj.attributes.mode
|
||||
@@ -32,7 +30,7 @@ class HaHumidifierState extends LitElement {
|
||||
: this._localizeState()}
|
||||
</div>
|
||||
|
||||
${currentStatus && !noValue
|
||||
${currentStatus && !isUnavailableState(this.stateObj.state)
|
||||
? html`<div class="current">
|
||||
${this.hass.localize("ui.card.humidifier.currently")}:
|
||||
<div class="unit">${currentStatus}</div>
|
||||
@@ -71,10 +69,7 @@ class HaHumidifierState extends LitElement {
|
||||
}
|
||||
|
||||
private _localizeState(): string {
|
||||
if (
|
||||
this.stateObj.state === UNAVAILABLE ||
|
||||
this.stateObj.state === UNKNOWN
|
||||
) {
|
||||
if (isUnavailableState(this.stateObj.state)) {
|
||||
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,10 @@ import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-icon-button";
|
||||
@@ -27,9 +26,7 @@ export interface IconOverflowMenuItem {
|
||||
|
||||
@customElement("ha-icon-overflow-menu")
|
||||
export class HaIconOverflowMenu extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Array }) public items: IconOverflowMenuItem[] = [];
|
||||
|
||||
@@ -47,7 +44,7 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
@click=${stopPropagation}
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this._localize("ui.common.overflow_menu")}
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
|
||||
@@ -14,7 +14,7 @@ class InputHelperText extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-size: 0.75rem;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding-inline-start: 16px;
|
||||
|
||||
@@ -8,11 +8,10 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { until } from "lit/directives/until";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { orderCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-icon-next";
|
||||
@@ -47,9 +46,7 @@ declare global {
|
||||
|
||||
@customElement("ha-items-display-editor")
|
||||
export class HaItemDisplayEditor extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public items: DisplayItem[] = [];
|
||||
|
||||
@@ -164,7 +161,7 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
? html`<ha-icon-button
|
||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||
slot="end"
|
||||
.label=${this._localize(
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.items-display-editor.${isVisible ? "hide" : "show"}`,
|
||||
{
|
||||
label: label,
|
||||
|
||||
@@ -167,8 +167,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clearable = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-sort" }) public noSort = false;
|
||||
|
||||
@query("lit-virtualizer") public virtualizerElement?: LitVirtualizer;
|
||||
|
||||
@query("ha-input-search") private _searchFieldElement?: HaInputSearch;
|
||||
@@ -344,7 +342,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
private _getItems = () => {
|
||||
let items = [...(this.getItems(this._search, this._selectedSection) || [])];
|
||||
|
||||
if (!this.sections?.length && !this.noSort) {
|
||||
if (!this.sections?.length) {
|
||||
items = items.sort((entityA, entityB) => {
|
||||
const sortLabelA =
|
||||
typeof entityA === "string" ? entityA : entityA.sorting_label;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { internationalizationContext, uiContext } from "../data/context";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "./radio/ha-radio-group";
|
||||
import "./radio/ha-radio-option";
|
||||
@@ -27,6 +26,8 @@ export interface SelectBoxOption {
|
||||
|
||||
@customElement("ha-select-box")
|
||||
export class HaSelectBox extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public options: SelectBoxOption[] = [];
|
||||
|
||||
@property({ attribute: false }) public value?: string;
|
||||
@@ -39,14 +40,6 @@ export class HaSelectBox extends LitElement {
|
||||
@property({ type: Boolean, attribute: "stacked_image" })
|
||||
public stackedImage = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
protected _i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
protected _ui?: ContextType<typeof uiContext>;
|
||||
|
||||
render() {
|
||||
const maxColumns = this.maxColumns ?? 3;
|
||||
const columns = Math.min(maxColumns, this.options.length);
|
||||
@@ -69,11 +62,11 @@ export class HaSelectBox extends LitElement {
|
||||
const disabled = option.disabled || this.disabled || false;
|
||||
const selected = option.value === this.value;
|
||||
|
||||
const isDark = this._ui?.themes.darkMode || false;
|
||||
const isRTL = this._i18n
|
||||
const isDark = this.hass?.themes.darkMode || false;
|
||||
const isRTL = this.hass
|
||||
? computeRTL(
|
||||
this._i18n.language,
|
||||
this._i18n.translationMetadata.translations
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
: false;
|
||||
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../common/translations/localize";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import type {
|
||||
AutomationBehavior,
|
||||
AutomationBehaviorConditionMode,
|
||||
AutomationBehaviorSelector,
|
||||
AutomationBehaviorTriggerMode,
|
||||
} from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-select-box";
|
||||
import type { SelectBoxOption } from "../ha-select-box";
|
||||
import "../ha-select-box";
|
||||
|
||||
const TRIGGER_BEHAVIORS: AutomationBehaviorTriggerMode[] = [
|
||||
"each",
|
||||
"any",
|
||||
"first",
|
||||
"all",
|
||||
"last",
|
||||
];
|
||||
|
||||
const CONDITION_BEHAVIORS: AutomationBehaviorConditionMode[] = ["any", "all"];
|
||||
|
||||
@customElement("ha-selector-automation_behavior")
|
||||
export class HaSelectorAutomationBehavior extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public selector!: AutomationBehaviorSelector;
|
||||
|
||||
@@ -40,9 +39,6 @@ export class HaSelectorAutomationBehavior extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@consumeLocalize()
|
||||
protected _localize?: LocalizeFunc;
|
||||
|
||||
protected render() {
|
||||
const { mode } = this.selector.automation_behavior ?? {};
|
||||
const modeKey = mode ?? "trigger";
|
||||
@@ -64,6 +60,7 @@ export class HaSelectorAutomationBehavior extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-select-box
|
||||
.hass=${this.hass}
|
||||
.options=${options}
|
||||
.value=${this.value ?? ""}
|
||||
max_columns="1"
|
||||
@@ -98,10 +95,8 @@ export class HaSelectorAutomationBehavior extends LitElement {
|
||||
return translated;
|
||||
}
|
||||
}
|
||||
return (
|
||||
this._localize?.(
|
||||
`ui.components.selectors.automation_behavior.${mode ?? "trigger"}.options.${behavior}.${field}` as LocalizeKeys
|
||||
) || behavior
|
||||
return this.hass.localize(
|
||||
`ui.components.selectors.automation_behavior.${mode ?? "trigger"}.options.${behavior}.${field}` as LocalizeKeys
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ColorTempSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-labeled-slider";
|
||||
@@ -94,10 +94,10 @@ export class HaColorTempSelector extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _valueChanged(ev: HASSDomEvent<HASSDomEvents["value-changed"]>) {
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: Number(ev.detail.value),
|
||||
value: Number((ev.detail as any).value),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { mdiPlayBox, mdiPlus } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import { getSignedPath } from "../../data/auth";
|
||||
import type { MediaPickedEvent } from "../../data/media-player";
|
||||
import {
|
||||
MediaClassBrowserSettings,
|
||||
@@ -12,10 +13,14 @@ import {
|
||||
} from "../../data/media-player";
|
||||
import type { MediaSelector, MediaSelectorValue } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
brandsUrl,
|
||||
extractDomainFromBrandUrl,
|
||||
isBrandUrl,
|
||||
} from "../../util/brands-url";
|
||||
import "../ha-alert";
|
||||
import "../ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../ha-form/types";
|
||||
import "../media-player/ha-media-browser-thumbnail";
|
||||
import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import "../ha-picture-upload";
|
||||
@@ -49,6 +54,8 @@ export class HaMediaSelector extends LitElement {
|
||||
filter_entity?: string | string[];
|
||||
};
|
||||
|
||||
@state() private _thumbnailUrl?: string | null;
|
||||
|
||||
private _contextEntities: string[] | undefined;
|
||||
|
||||
private get _hasAccept(): boolean {
|
||||
@@ -61,6 +68,35 @@ export class HaMediaSelector extends LitElement {
|
||||
this._contextEntities = ensureArray(this.context?.filter_entity);
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("value")) {
|
||||
const thumbnail = this.value?.metadata?.thumbnail;
|
||||
const oldThumbnail = (changedProps.get("value") as this["value"])
|
||||
?.metadata?.thumbnail;
|
||||
if (thumbnail === oldThumbnail) {
|
||||
return;
|
||||
}
|
||||
if (thumbnail && isBrandUrl(thumbnail)) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
this._thumbnailUrl = brandsUrl(
|
||||
{
|
||||
domain: extractDomainFromBrandUrl(thumbnail),
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
);
|
||||
} else if (thumbnail && thumbnail.startsWith("/")) {
|
||||
this._thumbnailUrl = undefined;
|
||||
// Thumbnails served by local API require authentication
|
||||
getSignedPath(this.hass, thumbnail).then((signedPath) => {
|
||||
this._thumbnailUrl = signedPath.path;
|
||||
});
|
||||
} else {
|
||||
this._thumbnailUrl = thumbnail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -150,12 +186,10 @@ export class HaMediaSelector extends LitElement {
|
||||
),
|
||||
})}
|
||||
image"
|
||||
>
|
||||
<ha-media-browser-thumbnail
|
||||
.hass=${this.hass}
|
||||
.url=${this.value.metadata.thumbnail}
|
||||
></ha-media-browser-thumbnail>
|
||||
</div>
|
||||
style=${this._thumbnailUrl
|
||||
? `background-image: url(${this._thumbnailUrl});`
|
||||
: ""}
|
||||
></div>
|
||||
`
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
@@ -376,11 +410,13 @@ export class HaMediaSelector extends LitElement {
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
--ha-media-browser-thumbnail-fit: cover;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.centered-image {
|
||||
margin: 4px;
|
||||
--ha-media-browser-thumbnail-fit: contain;
|
||||
background-size: contain;
|
||||
}
|
||||
.icon-holder {
|
||||
display: flex;
|
||||
|
||||
@@ -96,6 +96,7 @@ export class HaSelectSelector extends LitElement {
|
||||
.value=${this.value as string | undefined}
|
||||
@value-changed=${this._selectChanged}
|
||||
.maxColumns=${this.selector.select?.box_max_columns}
|
||||
.hass=${this.hass}
|
||||
></ha-select-box>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
@@ -198,7 +199,6 @@ export class HaSelectSelector extends LitElement {
|
||||
: nothing}
|
||||
|
||||
<ha-generic-picker
|
||||
no-sort
|
||||
.hass=${this.hass}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@@ -215,7 +215,6 @@ export class HaSelectSelector extends LitElement {
|
||||
if (this.selector.select?.custom_value) {
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
no-sort
|
||||
.hass=${this.hass}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
|
||||
@@ -837,7 +837,7 @@ export class HaServiceControl extends LitElement {
|
||||
if (targetDevices.length) {
|
||||
targetDevices = targetDevices.filter((device) =>
|
||||
deviceMeetsTargetSelector(
|
||||
this.hass.states,
|
||||
this.hass,
|
||||
Object.values(this.hass.entities),
|
||||
this.hass.devices[device],
|
||||
targetSelector
|
||||
|
||||
@@ -1,31 +1,24 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { configContext, connectionContext } from "../data/context";
|
||||
import {
|
||||
DEFAULT_SERVICE_ICON,
|
||||
FALLBACK_DOMAIN_ICONS,
|
||||
serviceIcon,
|
||||
} from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-service-icon")
|
||||
export class HaServiceIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public service?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
protected _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
protected _config?: ContextType<typeof configContext>;
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
@@ -35,13 +28,13 @@ export class HaServiceIcon extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this._connection || !this._config) {
|
||||
if (!this.hass) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = serviceIcon(
|
||||
this._connection.connection,
|
||||
this._config?.config,
|
||||
this.hass.connection,
|
||||
this.hass.config,
|
||||
this.service
|
||||
).then((icn) => {
|
||||
if (icn) {
|
||||
|
||||
@@ -62,7 +62,11 @@ class HaServicePicker extends LitElement {
|
||||
index
|
||||
) => html`
|
||||
<ha-combo-box-item type="button" .borderTop=${index !== 0}>
|
||||
<ha-service-icon slot="start" .service=${item.id}></ha-service-icon>
|
||||
<ha-service-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.service=${item.id}
|
||||
></ha-service-icon>
|
||||
<span slot="headline">${item.primary}</span>
|
||||
<span slot="supporting-text">${item.secondary}</span>
|
||||
${item.service_id && this.showServiceId
|
||||
@@ -108,7 +112,11 @@ class HaServicePicker extends LitElement {
|
||||
service;
|
||||
|
||||
return html`
|
||||
<ha-service-icon slot="start" .service=${serviceId}></ha-service-icon>
|
||||
<ha-service-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.service=${serviceId}
|
||||
></ha-service-icon>
|
||||
<span slot="headline">${serviceName}</span>
|
||||
${this.showServiceId
|
||||
? html`<span slot="supporting-text" class="code"
|
||||
|
||||
@@ -30,7 +30,6 @@ export class HaSettingsRow extends LitElement {
|
||||
<slot name="prefix"></slot>
|
||||
<div
|
||||
class="body"
|
||||
part="heading"
|
||||
?two-line=${!this.threeLine && hasDescription}
|
||||
?three-line=${this.threeLine}
|
||||
>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import "@home-assistant/webawesome/dist/components/textarea/textarea";
|
||||
import type WaTextarea from "@home-assistant/webawesome/dist/components/textarea/textarea";
|
||||
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { WaInputMixin, waInputStyles } from "./input/wa-input-mixin";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
|
||||
/**
|
||||
* Home Assistant textarea component
|
||||
@@ -85,20 +84,6 @@ export class HaTextArea extends WaInputMixin(LitElement) {
|
||||
this.removeEventListener("keydown", stopPropagation);
|
||||
}
|
||||
|
||||
protected override async firstUpdated(
|
||||
changedProperties: PropertyValues<this>
|
||||
): Promise<void> {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.autofocus) {
|
||||
await this._textarea?.updateComplete;
|
||||
this._textarea?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
public override focus(options?: FocusOptions): void {
|
||||
this._textarea?.focus(options);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const hasLabelSlot = this.label
|
||||
? false
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
|
||||
const DEFAULT_THEME = "default";
|
||||
|
||||
const SEARCH_KEYS = [{ name: "primary", weight: 1 }];
|
||||
|
||||
@customElement("ha-theme-picker")
|
||||
export class HaThemePicker extends LitElement {
|
||||
@property() public value?: string;
|
||||
@@ -29,74 +25,52 @@ export class HaThemePicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ attribute: "no-theme-label" }) public noThemeLabel?: string;
|
||||
|
||||
private _getThemeOptions = memoizeOne(
|
||||
(
|
||||
themes: Record<string, unknown>,
|
||||
locale: string,
|
||||
includeDefault: boolean
|
||||
): PickerComboBoxItem[] => {
|
||||
const items: PickerComboBoxItem[] = [];
|
||||
|
||||
if (includeDefault) {
|
||||
items.push({ id: DEFAULT_THEME, primary: "Home Assistant" });
|
||||
}
|
||||
|
||||
const themeNames = Object.keys(themes).sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a, b, locale)
|
||||
);
|
||||
for (const theme of themeNames) {
|
||||
items.push({ id: theme, primary: theme });
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _getItems = () =>
|
||||
this._getThemeOptions(
|
||||
this.hass?.themes.themes || {},
|
||||
this.hass?.locale.language || "en",
|
||||
this.includeDefault
|
||||
);
|
||||
|
||||
private _valueRenderer = (value: string): TemplateResult =>
|
||||
html`<span slot="headline"
|
||||
>${this._getItems().find((i) => i.id === value)?.primary ?? value}</span
|
||||
>`;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const options: HaSelectOption[] = Object.keys(
|
||||
this.hass?.themes.themes || {}
|
||||
).map((theme) => ({
|
||||
value: theme,
|
||||
}));
|
||||
|
||||
if (this.includeDefault) {
|
||||
options.unshift({
|
||||
value: DEFAULT_THEME,
|
||||
label: "Home Assistant",
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.required) {
|
||||
options.unshift({
|
||||
value: "remove",
|
||||
label: this.hass!.localize("ui.components.theme-picker.no_theme"),
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.label=${this.label ??
|
||||
this.hass?.localize("ui.components.theme-picker.theme") ??
|
||||
"Theme"}
|
||||
.placeholder=${this.noThemeLabel ??
|
||||
this.hass?.localize("ui.components.theme-picker.no_theme")}
|
||||
.helper=${this.helper}
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass!.localize("ui.components.theme-picker.theme")}
|
||||
.value=${this.value}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.getItems=${this._getItems}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.helper}
|
||||
.required=${this.required}
|
||||
@value-changed=${this._changed}
|
||||
popover-placement="bottom"
|
||||
></ha-generic-picker>
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
.options=${options}
|
||||
></ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-generic-picker {
|
||||
ha-select {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: ValueChangedEvent<string | undefined>): void {
|
||||
ev.stopPropagation();
|
||||
this.value = ev.detail.value;
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
if (!this.hass || ev.detail.value === "") {
|
||||
return;
|
||||
}
|
||||
this.value = ev.detail.value === "remove" ? undefined : ev.detail.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import { mdiLightbulbOutline } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-tip")
|
||||
class HaTip extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
public render() {
|
||||
if (!this._localize) {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-svg-icon .path=${mdiLightbulbOutline}></ha-svg-icon>
|
||||
<span class="prefix">${this._localize("ui.panel.config.tips.tip")}</span>
|
||||
<span class="prefix"
|
||||
>${this.hass.localize("ui.panel.config.tips.tip")}</span
|
||||
>
|
||||
<span class="text"><slot></slot></span>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,230 +1,64 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
|
||||
const PASSIVE_EVENT_OPTIONS = { passive: true } as const;
|
||||
|
||||
export const haTopAppBarFixedStyles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.top-app-bar {
|
||||
box-sizing: border-box;
|
||||
color: var(--app-header-text-color, #fff);
|
||||
background-color: var(--app-header-background-color, var(--primary-color));
|
||||
position: fixed;
|
||||
top: 0;
|
||||
inset-inline-end: 0;
|
||||
width: var(--ha-top-app-bar-width, 100%);
|
||||
z-index: 4;
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
transition:
|
||||
width var(--ha-animation-duration-normal) ease,
|
||||
padding-left var(--ha-animation-duration-normal) ease,
|
||||
padding-right var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
|
||||
:host([narrow]) .top-app-bar {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
|
||||
.top-app-bar.scrolled:not(.pane-header) {
|
||||
box-shadow: var(--ha-box-shadow-s);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: var(--header-height);
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
padding: 0 var(--ha-space-3);
|
||||
}
|
||||
|
||||
#navigation {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.section.center {
|
||||
flex: 1 1 auto;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section.end {
|
||||
flex: none;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: var(--header-height);
|
||||
padding-inline-start: var(--ha-space-6);
|
||||
}
|
||||
|
||||
:host([narrow]) .title {
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.top-app-bar-fixed-adjust {
|
||||
padding-top: calc(
|
||||
var(--header-height, 0px) + var(--safe-area-inset-top, 0px)
|
||||
);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
|
||||
:host([narrow]) .top-app-bar-fixed-adjust {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
`;
|
||||
import { TopAppBarFixedBase } from "@material/mwc-top-app-bar-fixed/mwc-top-app-bar-fixed-base";
|
||||
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-top-app-bar-fixed")
|
||||
export class HaTopAppBarFixed extends LitElement {
|
||||
export class HaTopAppBarFixed extends TopAppBarFixedBase {
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ attribute: "center-title", type: Boolean }) centerTitle = false;
|
||||
|
||||
@query(".top-app-bar") protected _barElement!: HTMLElement;
|
||||
|
||||
private _scrollTarget?: HTMLElement | Window;
|
||||
|
||||
@property({ attribute: false })
|
||||
public get scrollTarget(): HTMLElement | Window {
|
||||
return this._scrollTarget || window;
|
||||
}
|
||||
|
||||
public set scrollTarget(value: HTMLElement | Window) {
|
||||
const old = this.scrollTarget;
|
||||
this._unregisterListeners();
|
||||
this._scrollTarget = value;
|
||||
this._updateBarPosition();
|
||||
this.requestUpdate("scrollTarget", old);
|
||||
if (this.isConnected) {
|
||||
this._registerListeners();
|
||||
this._syncScrollState();
|
||||
}
|
||||
}
|
||||
|
||||
protected _isPaneHeader(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`${this._renderHeader()}${this._renderContent()}`;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
if (this.hasUpdated) {
|
||||
this._updateBarPosition();
|
||||
this._registerListeners();
|
||||
this._syncScrollState();
|
||||
}
|
||||
}
|
||||
|
||||
protected _renderHeader() {
|
||||
const title = html`<span class="title">
|
||||
<slot name="title"></slot>
|
||||
</span>`;
|
||||
const paneHeader = this._isPaneHeader();
|
||||
|
||||
return html`
|
||||
<header
|
||||
class="top-app-bar ${classMap({
|
||||
"pane-header": paneHeader,
|
||||
})}"
|
||||
>
|
||||
<div class="row">
|
||||
${paneHeader
|
||||
? html`<section class="section" id="title">
|
||||
<slot name="navigationIcon"></slot>
|
||||
${title}
|
||||
</section>`
|
||||
: nothing}
|
||||
<section class="section" id="navigation">
|
||||
${paneHeader
|
||||
? nothing
|
||||
: html`<slot name="navigationIcon"></slot> ${this.centerTitle
|
||||
? nothing
|
||||
: title}`}
|
||||
</section>
|
||||
${!paneHeader && this.centerTitle
|
||||
? html`<section class="section center">${title}</section>`
|
||||
: nothing}
|
||||
<section class="section end" id="actions" role="toolbar">
|
||||
<slot name="actionItems"></slot>
|
||||
</section>
|
||||
</div>
|
||||
</header>
|
||||
`;
|
||||
}
|
||||
|
||||
protected _renderContent() {
|
||||
return html`<div class="top-app-bar-fixed-adjust">
|
||||
<slot></slot>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._updateBarPosition();
|
||||
this._registerListeners();
|
||||
this._syncScrollState();
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unregisterListeners();
|
||||
}
|
||||
|
||||
protected _updateBarPosition() {
|
||||
if (this._barElement) {
|
||||
this._barElement.style.position =
|
||||
this.scrollTarget === window ? "" : "absolute";
|
||||
}
|
||||
}
|
||||
|
||||
protected _syncScrollState = () => {
|
||||
const scrollTop =
|
||||
this.scrollTarget instanceof Window
|
||||
? this.scrollTarget.pageYOffset
|
||||
: this.scrollTarget.scrollTop;
|
||||
this._barElement?.classList.toggle("scrolled", scrollTop > 0);
|
||||
};
|
||||
|
||||
protected _registerListeners() {
|
||||
this.scrollTarget.addEventListener(
|
||||
"scroll",
|
||||
this._syncScrollState,
|
||||
PASSIVE_EVENT_OPTIONS
|
||||
);
|
||||
}
|
||||
|
||||
protected _unregisterListeners() {
|
||||
this.scrollTarget.removeEventListener("scroll", this._syncScrollState);
|
||||
}
|
||||
|
||||
static override styles: CSSResultGroup = haTopAppBarFixedStyles;
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
header {
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
}
|
||||
.mdc-top-app-bar__row {
|
||||
height: var(--header-height);
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
}
|
||||
.mdc-top-app-bar--fixed-adjust {
|
||||
padding-top: calc(
|
||||
var(--header-height, 0px) + var(--safe-area-inset-top, 0px)
|
||||
);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
:host([narrow]) .mdc-top-app-bar--fixed-adjust {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
.mdc-top-app-bar {
|
||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--app-header-text-color, var(--mdc-theme-on-primary, #fff));
|
||||
background-color: var(
|
||||
--app-header-background-color,
|
||||
var(--mdc-theme-primary)
|
||||
);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
transition:
|
||||
width var(--ha-animation-duration-normal) ease,
|
||||
padding-left var(--ha-animation-duration-normal) ease,
|
||||
padding-right var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
:host([narrow]) .mdc-top-app-bar {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mdc-top-app-bar {
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
.mdc-top-app-bar__title {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
padding-inline-start: var(--ha-space-6);
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
:host([narrow]) .mdc-top-app-bar__title {
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { TopAppBarBase } from "@material/mwc-top-app-bar/mwc-top-app-bar-base";
|
||||
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-top-app-bar")
|
||||
export class HaTopAppBar extends TopAppBarBase {
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
.mdc-top-app-bar__row {
|
||||
height: var(--header-height);
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
}
|
||||
.mdc-top-app-bar--fixed-adjust {
|
||||
padding-top: calc(var(--safe-area-inset-top) + var(--header-height));
|
||||
}
|
||||
.mdc-top-app-bar {
|
||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--app-header-text-color, var(--mdc-theme-on-primary, #fff));
|
||||
background-color: var(
|
||||
--app-header-background-color,
|
||||
var(--mdc-theme-primary)
|
||||
);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-top-app-bar": HaTopAppBar;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,136 @@
|
||||
import {
|
||||
addHasRemoveClass,
|
||||
BaseElement,
|
||||
} from "@material/mwc-base/base-element";
|
||||
import { supportsPassiveEventListener } from "@material/mwc-base/utils";
|
||||
import type { MDCTopAppBarAdapter } from "@material/top-app-bar/adapter";
|
||||
import { strings } from "@material/top-app-bar/constants";
|
||||
// eslint-disable-next-line import-x/no-named-as-default
|
||||
import MDCFixedTopAppBarFoundation from "@material/top-app-bar/fixed/foundation";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, css, nothing } from "lit";
|
||||
import { property, query, customElement } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import {
|
||||
HaTopAppBarFixed,
|
||||
haTopAppBarFixedStyles,
|
||||
} from "./ha-top-app-bar-fixed";
|
||||
|
||||
const PASSIVE_EVENT_OPTIONS = { passive: true } as const;
|
||||
export const passiveEventOptionsIfSupported = supportsPassiveEventListener
|
||||
? { passive: true }
|
||||
: undefined;
|
||||
|
||||
@customElement("ha-two-pane-top-app-bar-fixed")
|
||||
export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
export class TopAppBarBaseBase extends BaseElement {
|
||||
protected override mdcFoundation!: MDCFixedTopAppBarFoundation;
|
||||
|
||||
protected override mdcFoundationClass = MDCFixedTopAppBarFoundation;
|
||||
|
||||
@query(".mdc-top-app-bar") protected mdcRoot!: HTMLElement;
|
||||
|
||||
// _actionItemsSlot should have type HTMLSlotElement, but when TypeScript's
|
||||
// emitDecoratorMetadata is enabled, the HTMLSlotElement constructor will
|
||||
// be emitted into the runtime, which will cause an "HTMLSlotElement is
|
||||
// undefined" error in browsers that don't define it (e.g. IE11).
|
||||
@query('slot[name="actionItems"]') protected _actionItemsSlot!: HTMLElement;
|
||||
|
||||
protected _scrollTarget!: HTMLElement | Window;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ attribute: "center-title", type: Boolean }) centerTitle = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) prominent = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) dense = false;
|
||||
|
||||
@property({ type: Boolean }) pane = false;
|
||||
|
||||
@property({ type: Boolean }) footer = false;
|
||||
|
||||
@query(".content") private _contentElement?: HTMLElement;
|
||||
@query(".content") private _contentElement!: HTMLElement;
|
||||
|
||||
@query(".pane .ha-scrollbar") private _paneElement?: HTMLElement;
|
||||
|
||||
protected override _isPaneHeader(): boolean {
|
||||
return this.pane;
|
||||
@property({ attribute: false })
|
||||
get scrollTarget() {
|
||||
return this._scrollTarget || window;
|
||||
}
|
||||
|
||||
protected override _renderContent() {
|
||||
set scrollTarget(value) {
|
||||
this.unregisterListeners();
|
||||
const old = this.scrollTarget;
|
||||
this._scrollTarget = value;
|
||||
this.updateRootPosition();
|
||||
this.requestUpdate("scrollTarget", old);
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
protected updateRootPosition() {
|
||||
if (this.mdcRoot) {
|
||||
const windowScroller = this.scrollTarget === window;
|
||||
// we add support for top-app-bar's tied to an element scroller.
|
||||
this.mdcRoot.style.position = windowScroller ? "" : "absolute";
|
||||
}
|
||||
}
|
||||
|
||||
protected barClasses() {
|
||||
return {
|
||||
"mdc-top-app-bar--dense": this.dense,
|
||||
"mdc-top-app-bar--prominent": this.prominent,
|
||||
"center-title": this.centerTitle,
|
||||
"mdc-top-app-bar--fixed": true,
|
||||
"mdc-top-app-bar--pane": this.pane,
|
||||
};
|
||||
}
|
||||
|
||||
protected contentClasses() {
|
||||
return {
|
||||
"mdc-top-app-bar--fixed-adjust": !this.dense && !this.prominent,
|
||||
"mdc-top-app-bar--dense-fixed-adjust": this.dense && !this.prominent,
|
||||
"mdc-top-app-bar--prominent-fixed-adjust": !this.dense && this.prominent,
|
||||
"mdc-top-app-bar--dense-prominent-fixed-adjust":
|
||||
this.dense && this.prominent,
|
||||
"mdc-top-app-bar--pane": this.pane,
|
||||
};
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
const title = html`<span class="mdc-top-app-bar__title"
|
||||
><slot name="title"></slot
|
||||
></span>`;
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
"top-app-bar-fixed-adjust": true,
|
||||
"top-app-bar-fixed-adjust--pane": this.pane,
|
||||
})}
|
||||
>
|
||||
<header class="mdc-top-app-bar ${classMap(this.barClasses())}">
|
||||
<div class="mdc-top-app-bar__row">
|
||||
${this.pane
|
||||
? html`<section
|
||||
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-start"
|
||||
id="title"
|
||||
>
|
||||
<slot
|
||||
name="navigationIcon"
|
||||
@click=${this.handleNavigationClick}
|
||||
></slot>
|
||||
${title}
|
||||
</section>`
|
||||
: nothing}
|
||||
<section class="mdc-top-app-bar__section" id="navigation">
|
||||
${this.pane
|
||||
? nothing
|
||||
: html`<slot
|
||||
name="navigationIcon"
|
||||
@click=${this.handleNavigationClick}
|
||||
></slot
|
||||
>${title}`}
|
||||
</section>
|
||||
<section
|
||||
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-end"
|
||||
id="actions"
|
||||
role="toolbar"
|
||||
>
|
||||
<slot name="actionItems"></slot>
|
||||
</section>
|
||||
</div>
|
||||
</header>
|
||||
<div class=${classMap(this.contentClasses())}>
|
||||
${this.pane
|
||||
? html`<div class="pane">
|
||||
<div class="shadow-container"></div>
|
||||
@@ -55,57 +154,117 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
`;
|
||||
}
|
||||
|
||||
protected override willUpdate(changedProperties: PropertyValues<this>) {
|
||||
super.willUpdate(changedProperties);
|
||||
if (changedProperties.has("pane") && this.hasUpdated) {
|
||||
this._unregisterListeners();
|
||||
}
|
||||
}
|
||||
|
||||
protected override updated(changedProperties: PropertyValues<this>) {
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has("pane") &&
|
||||
changedProperties.get("pane") !== undefined
|
||||
) {
|
||||
this._registerListeners();
|
||||
this._syncScrollState();
|
||||
this.unregisterListeners();
|
||||
this.registerListeners();
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePaneScroll = (ev: Event) => {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
target.parentElement?.classList.toggle("scrolled", target.scrollTop > 0);
|
||||
protected createAdapter(): MDCTopAppBarAdapter {
|
||||
return {
|
||||
...addHasRemoveClass(this.mdcRoot),
|
||||
setStyle: (prprty: string, value: string) =>
|
||||
this.mdcRoot.style.setProperty(prprty, value),
|
||||
getTopAppBarHeight: () => this.mdcRoot.clientHeight,
|
||||
notifyNavigationIconClicked: () => {
|
||||
this.dispatchEvent(
|
||||
new Event(strings.NAVIGATION_EVENT, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
);
|
||||
},
|
||||
getViewportScrollY: () =>
|
||||
this.scrollTarget instanceof Window
|
||||
? this.scrollTarget.pageYOffset
|
||||
: this.scrollTarget.scrollTop,
|
||||
getTotalActionItems: () =>
|
||||
(this._actionItemsSlot as HTMLSlotElement).assignedNodes({
|
||||
flatten: true,
|
||||
}).length,
|
||||
};
|
||||
}
|
||||
|
||||
protected handleTargetScroll = () => {
|
||||
this.mdcFoundation.handleTargetScroll();
|
||||
};
|
||||
|
||||
protected override _registerListeners() {
|
||||
protected handlePaneScroll = (ev) => {
|
||||
if (ev.target.scrollTop > 0) {
|
||||
ev.target.parentElement.classList.add("scrolled");
|
||||
} else {
|
||||
ev.target.parentElement.classList.remove("scrolled");
|
||||
}
|
||||
};
|
||||
|
||||
protected handleNavigationClick = () => {
|
||||
this.mdcFoundation.handleNavigationClick();
|
||||
};
|
||||
|
||||
protected registerListeners() {
|
||||
if (this.pane) {
|
||||
this._paneElement?.addEventListener(
|
||||
this._paneElement!.addEventListener(
|
||||
"scroll",
|
||||
this._handlePaneScroll,
|
||||
PASSIVE_EVENT_OPTIONS
|
||||
this.handlePaneScroll,
|
||||
passiveEventOptionsIfSupported
|
||||
);
|
||||
this._contentElement?.addEventListener(
|
||||
this._contentElement.addEventListener(
|
||||
"scroll",
|
||||
this._handlePaneScroll,
|
||||
PASSIVE_EVENT_OPTIONS
|
||||
this.handlePaneScroll,
|
||||
passiveEventOptionsIfSupported
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
super._registerListeners();
|
||||
this.scrollTarget.addEventListener(
|
||||
"scroll",
|
||||
this.handleTargetScroll,
|
||||
passiveEventOptionsIfSupported
|
||||
);
|
||||
}
|
||||
|
||||
protected override _unregisterListeners() {
|
||||
this._paneElement?.removeEventListener("scroll", this._handlePaneScroll);
|
||||
this._contentElement?.removeEventListener("scroll", this._handlePaneScroll);
|
||||
super._unregisterListeners();
|
||||
protected unregisterListeners() {
|
||||
this._paneElement?.removeEventListener("scroll", this.handlePaneScroll);
|
||||
this._contentElement.removeEventListener("scroll", this.handlePaneScroll);
|
||||
this.scrollTarget.removeEventListener("scroll", this.handleTargetScroll);
|
||||
}
|
||||
|
||||
protected override firstUpdated() {
|
||||
super.firstUpdated();
|
||||
this.updateRootPosition();
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.unregisterListeners();
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
haTopAppBarFixedStyles,
|
||||
styles,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
header {
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
}
|
||||
.mdc-top-app-bar__row {
|
||||
height: var(--header-height);
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
}
|
||||
.mdc-top-app-bar--fixed-adjust {
|
||||
padding-top: calc(
|
||||
var(--header-height, 0px) + var(--safe-area-inset-top, 0px)
|
||||
);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
:host([narrow]) .mdc-top-app-bar--fixed-adjust {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
.shadow-container {
|
||||
position: absolute;
|
||||
top: calc(-1 * var(--header-height));
|
||||
@@ -114,11 +273,39 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
z-index: 1;
|
||||
transition: box-shadow 200ms linear;
|
||||
}
|
||||
|
||||
.scrolled .shadow-container {
|
||||
box-shadow: var(--ha-box-shadow-m);
|
||||
box-shadow: var(
|
||||
--mdc-top-app-bar-fixed-box-shadow,
|
||||
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
|
||||
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
|
||||
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
|
||||
);
|
||||
}
|
||||
.mdc-top-app-bar {
|
||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--app-header-text-color, var(--mdc-theme-on-primary, #fff));
|
||||
background-color: var(
|
||||
--app-header-background-color,
|
||||
var(--mdc-theme-primary)
|
||||
);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
transition:
|
||||
width var(--ha-animation-duration-normal) ease,
|
||||
padding-left var(--ha-animation-duration-normal) ease,
|
||||
padding-right var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
:host([narrow]) .mdc-top-app-bar {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mdc-top-app-bar {
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
.mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#title {
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-inline-end: 1px solid rgba(255, 255, 255, 0.12);
|
||||
@@ -127,8 +314,7 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
flex: 0 0 var(--sidepane-width, 250px);
|
||||
width: var(--sidepane-width, 250px);
|
||||
}
|
||||
|
||||
.top-app-bar-fixed-adjust--pane {
|
||||
div.mdc-top-app-bar--pane {
|
||||
display: flex;
|
||||
height: calc(
|
||||
100vh - var(--header-height, 0px) - var(
|
||||
@@ -137,7 +323,6 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
) - var(--safe-area-inset-bottom, 0px)
|
||||
);
|
||||
}
|
||||
|
||||
.pane {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
border-inline-end: 1px solid var(--divider-color);
|
||||
@@ -149,36 +334,36 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pane .ha-scrollbar {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pane .footer {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.main {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.top-app-bar-fixed-adjust--pane .main {
|
||||
.mdc-top-app-bar--pane .main {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.top-app-bar-fixed-adjust--pane .content {
|
||||
.mdc-top-app-bar--pane .content {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.mdc-top-app-bar__title {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
padding-inline-start: 24px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-two-pane-top-app-bar-fixed": HaTwoPaneTopAppBarFixed;
|
||||
"ha-two-pane-top-app-bar-fixed": TopAppBarBaseBase;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type LitElement, css } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { nativeElementInternalsSupported } from "../../common/feature-detect/support-native-element-internals";
|
||||
import type { Constructor } from "../../types";
|
||||
import { nativeElementInternalsSupported } from "../../common/feature-detect/support-native-element-internals";
|
||||
|
||||
/**
|
||||
* Minimal interface for the inner wa-input / wa-textarea element.
|
||||
@@ -339,7 +339,7 @@ export const waInputStyles = css`
|
||||
min-height: var(--ha-space-5);
|
||||
margin-block-start: 0;
|
||||
margin-inline-start: var(--ha-space-3);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--ha-color-text-secondary);
|
||||
|
||||
@@ -227,7 +227,7 @@ class DialogMediaManage extends LitElement {
|
||||
</ha-list>
|
||||
`}
|
||||
${isComponentLoaded(this.hass.config, "hassio")
|
||||
? html`<ha-tip>
|
||||
? html`<ha-tip .hass=${this.hass}>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.tip_media_storage",
|
||||
{
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
brandsUrl,
|
||||
extractDomainFromBrandUrl,
|
||||
isBrandUrl,
|
||||
} from "../../util/brands-url";
|
||||
|
||||
const SMALL_THUMBNAIL_THRESHOLD = 16;
|
||||
|
||||
const isSvgUrl = (url: string): boolean =>
|
||||
/\.svg(\?|#|$)/i.test(url) || url.startsWith("data:image/svg+xml");
|
||||
|
||||
const resolveThumbnailURL = (
|
||||
hass: HomeAssistant,
|
||||
thumbnailUrl: string
|
||||
): Promise<string> => {
|
||||
if (isBrandUrl(thumbnailUrl)) {
|
||||
return Promise.resolve(
|
||||
brandsUrl(
|
||||
{
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
darkOptimized: hass.themes?.darkMode,
|
||||
},
|
||||
hass.auth.data.hassUrl
|
||||
)
|
||||
);
|
||||
}
|
||||
if (thumbnailUrl.startsWith("/")) {
|
||||
// Local thumbnails require authentication; fetch and inline as base64.
|
||||
return hass
|
||||
.fetchWithAuth(thumbnailUrl)
|
||||
.then((response) => response.blob())
|
||||
.then(
|
||||
(blob) =>
|
||||
new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () =>
|
||||
resolve(typeof reader.result === "string" ? reader.result : "");
|
||||
reader.onerror = (e) => reject(e);
|
||||
reader.readAsDataURL(blob);
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.resolve(thumbnailUrl);
|
||||
};
|
||||
|
||||
@customElement("ha-media-browser-thumbnail")
|
||||
export class HaMediaBrowserThumbnail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public url?: string;
|
||||
|
||||
@state() private _resolvedUrl?: string;
|
||||
|
||||
@state() private _small = false;
|
||||
|
||||
@state() private _brand = false;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("url")) {
|
||||
this._resolve();
|
||||
}
|
||||
}
|
||||
|
||||
private async _resolve(): Promise<void> {
|
||||
this._small = false;
|
||||
this._brand = !!this.url && isBrandUrl(this.url);
|
||||
if (!this.url) {
|
||||
this._resolvedUrl = undefined;
|
||||
return;
|
||||
}
|
||||
const requested = this.url;
|
||||
try {
|
||||
const resolved = await resolveThumbnailURL(this.hass, requested);
|
||||
if (requested !== this.url) return;
|
||||
this._resolvedUrl = resolved;
|
||||
this._probeSize(resolved);
|
||||
} catch (_err) {
|
||||
if (requested === this.url) this._resolvedUrl = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _probeSize(url: string): void {
|
||||
// SVGs (including brand icons) scale natively; pixelated rendering would
|
||||
// break vector output.
|
||||
if (this.url && isBrandUrl(this.url)) return;
|
||||
if (isSvgUrl(url)) return;
|
||||
const img = new Image();
|
||||
img.addEventListener("load", () => {
|
||||
if (this._resolvedUrl !== url) return;
|
||||
if (
|
||||
img.naturalWidth > 0 &&
|
||||
img.naturalWidth <= SMALL_THUMBNAIL_THRESHOLD
|
||||
) {
|
||||
this._small = true;
|
||||
}
|
||||
});
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this._resolvedUrl) return nothing;
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
image: true,
|
||||
small: this._small,
|
||||
brand: this._brand,
|
||||
})}
|
||||
style="background-image: url(${this._resolvedUrl})"
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
|
||||
static readonly styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: var(--ha-media-browser-thumbnail-fit, contain);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.image.brand {
|
||||
background-size: 40%;
|
||||
}
|
||||
.image.small {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-media-browser-thumbnail": HaMediaBrowserThumbnail;
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,11 @@ import {
|
||||
} from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { until } from "lit/directives/until";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { slugify } from "../../common/string/slugify";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { UNAVAILABLE } from "../../data/entity/entity";
|
||||
import { isUnavailableState } from "../../data/entity/entity";
|
||||
import type {
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
@@ -38,6 +39,11 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
brandsUrl,
|
||||
extractDomainFromBrandUrl,
|
||||
isBrandUrl,
|
||||
} from "../../util/brands-url";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import "../entity/ha-entity-picker";
|
||||
import "../ha-alert";
|
||||
@@ -46,7 +52,6 @@ import "../ha-card";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-list";
|
||||
import "../ha-list-item";
|
||||
import "./ha-media-browser-thumbnail";
|
||||
import "../ha-spinner";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tooltip";
|
||||
@@ -285,7 +290,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
} else if (
|
||||
err.code === "entity_not_found" &&
|
||||
this.entityId &&
|
||||
this.hass.states[this.entityId]?.state === UNAVAILABLE
|
||||
isUnavailableState(this.hass.states[this.entityId]?.state)
|
||||
) {
|
||||
this._setError({
|
||||
message: this.hass.localize(
|
||||
@@ -406,6 +411,12 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
? MediaClassBrowserSettings[currentItem.children_media_class]
|
||||
: MediaClassBrowserSettings.directory;
|
||||
|
||||
const backgroundImage = currentItem.thumbnail
|
||||
? this._getThumbnailURLorBase64(currentItem.thumbnail).then(
|
||||
(value) => `url(${value})`
|
||||
)
|
||||
: "none";
|
||||
|
||||
return html`
|
||||
${
|
||||
currentItem.can_play
|
||||
@@ -420,11 +431,13 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
<div class="header-content">
|
||||
${currentItem.thumbnail
|
||||
? html`
|
||||
<div class="img">
|
||||
<ha-media-browser-thumbnail
|
||||
.hass=${this.hass}
|
||||
.url=${currentItem.thumbnail}
|
||||
></ha-media-browser-thumbnail>
|
||||
<div
|
||||
class="img"
|
||||
style="background-image: ${until(
|
||||
backgroundImage,
|
||||
""
|
||||
)}"
|
||||
>
|
||||
${this.narrow &&
|
||||
currentItem?.can_play &&
|
||||
(!this.accept ||
|
||||
@@ -625,6 +638,12 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
private _renderGridItem = (child: MediaPlayerItem): TemplateResult => {
|
||||
const backgroundImage = child.thumbnail
|
||||
? this._getThumbnailURLorBase64(child.thumbnail).then(
|
||||
(value) => `url(${value})`
|
||||
)
|
||||
: "none";
|
||||
|
||||
return html`
|
||||
<div class="child" .item=${child} @click=${this._childClicked}>
|
||||
<ha-card outlined>
|
||||
@@ -636,13 +655,10 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
"centered-image": ["app", "directory"].includes(
|
||||
child.media_class
|
||||
),
|
||||
"brand-image": isBrandUrl(child.thumbnail),
|
||||
})} image"
|
||||
>
|
||||
<ha-media-browser-thumbnail
|
||||
.hass=${this.hass}
|
||||
.url=${child.thumbnail}
|
||||
></ha-media-browser-thumbnail>
|
||||
</div>
|
||||
style="background-image: ${until(backgroundImage, "")}"
|
||||
></div>
|
||||
`
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
@@ -687,7 +703,13 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
private _renderListItem = (child: MediaPlayerItem): TemplateResult => {
|
||||
const currentItem = this._currentItem;
|
||||
const mediaClass = MediaClassBrowserSettings[currentItem!.media_class];
|
||||
const showImage = mediaClass.show_list_images && !!child.thumbnail;
|
||||
|
||||
const backgroundImage =
|
||||
mediaClass.show_list_images && child.thumbnail
|
||||
? this._getThumbnailURLorBase64(child.thumbnail).then(
|
||||
(value) => `url(${value})`
|
||||
)
|
||||
: "none";
|
||||
|
||||
return html`
|
||||
<ha-list-item
|
||||
@@ -695,7 +717,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
.item=${child}
|
||||
.graphic=${mediaClass.show_list_images ? "medium" : "avatar"}
|
||||
>
|
||||
${!showImage && !child.can_play
|
||||
${backgroundImage === "none" && !child.can_play
|
||||
? html`<ha-svg-icon
|
||||
.path=${MediaClassBrowserSettings[
|
||||
child.media_class === "directory"
|
||||
@@ -709,14 +731,9 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
graphic: true,
|
||||
thumbnail: mediaClass.show_list_images === true,
|
||||
})}
|
||||
style="background-image: ${until(backgroundImage, "")}"
|
||||
slot="graphic"
|
||||
>
|
||||
${showImage
|
||||
? html`<ha-media-browser-thumbnail
|
||||
.hass=${this.hass}
|
||||
.url=${child.thumbnail}
|
||||
></ha-media-browser-thumbnail>`
|
||||
: nothing}
|
||||
${child.can_play
|
||||
? html`<ha-icon-button
|
||||
class="play ${classMap({
|
||||
@@ -736,6 +753,51 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
`;
|
||||
};
|
||||
|
||||
private async _getThumbnailURLorBase64(
|
||||
thumbnailUrl: string | undefined
|
||||
): Promise<string> {
|
||||
if (!thumbnailUrl) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (isBrandUrl(thumbnailUrl)) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
return brandsUrl(
|
||||
{
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
);
|
||||
}
|
||||
|
||||
if (thumbnailUrl.startsWith("/")) {
|
||||
// Thumbnails served by local API require authentication
|
||||
return new Promise((resolve, reject) => {
|
||||
this.hass
|
||||
.fetchWithAuth(thumbnailUrl!)
|
||||
// Since we are fetching with an authorization header, we cannot just put the
|
||||
// URL directly into the document; we need to embed the image. We could do this
|
||||
// using blob URLs, but then we would need to keep track of them in order to
|
||||
// release them properly. Instead, we embed the thumbnail using base64.
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const result = reader.result;
|
||||
resolve(typeof result === "string" ? result : "");
|
||||
};
|
||||
reader.onerror = (e) => reject(e);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
private _actionClicked = (ev: MouseEvent): void => {
|
||||
ev.stopPropagation();
|
||||
const item = (ev.currentTarget as any).item;
|
||||
@@ -986,20 +1048,14 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.header-content .img {
|
||||
position: relative;
|
||||
height: 175px;
|
||||
width: 175px;
|
||||
margin-right: 16px;
|
||||
background-size: cover;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
width 0.4s,
|
||||
height 0.4s;
|
||||
--ha-media-browser-thumbnail-fit: cover;
|
||||
}
|
||||
.header-content .img ha-media-browser-thumbnail {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
.header-info {
|
||||
display: flex;
|
||||
@@ -1135,12 +1191,18 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
--ha-media-browser-thumbnail-fit: cover;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.centered-image {
|
||||
margin: 0 8px;
|
||||
--ha-media-browser-thumbnail-fit: contain;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.brand-image {
|
||||
background-size: 40%;
|
||||
}
|
||||
|
||||
.children ha-card .icon-holder {
|
||||
@@ -1216,21 +1278,17 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
ha-list-item .graphic {
|
||||
position: relative;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
ha-list-item .graphic ha-media-browser-thumbnail {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
ha-list-item .graphic .play {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
margin: auto;
|
||||
opacity: 0;
|
||||
transition: all 0.5s;
|
||||
background-color: rgba(var(--rgb-card-background-color), 0.5);
|
||||
|
||||
@@ -99,8 +99,6 @@ export class HaRadioOption extends Radio {
|
||||
--ha-radio-option-checked-background-color,
|
||||
var(--ha-color-fill-primary-normal-resting)
|
||||
);
|
||||
color: var(--ha-color-fill-primary-loud-resting);
|
||||
border-color: var(--ha-color-fill-primary-loud-resting);
|
||||
}
|
||||
|
||||
[part~="label"] {
|
||||
|
||||
@@ -454,7 +454,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
}
|
||||
try {
|
||||
const entries = await extractFromTarget(
|
||||
this.hass.callWS,
|
||||
this.hass,
|
||||
{
|
||||
[`${this.type}_id`]: [this.itemId],
|
||||
},
|
||||
|
||||
@@ -112,7 +112,6 @@ export class HaTileContainer extends LitElement {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.vertical ::slotted([slot="info"]) {
|
||||
width: 100%;
|
||||
|
||||
@@ -451,6 +451,7 @@ export class HatScriptGraph extends LitElement {
|
||||
${node.action
|
||||
? html`<ha-service-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.service=${node.action}
|
||||
></ha-service-icon>`
|
||||
: nothing}
|
||||
|
||||
@@ -62,7 +62,7 @@ export const AREA_CONTROLS_BUTTONS: Record<
|
||||
};
|
||||
|
||||
export const getAreaControlEntities = (
|
||||
controls: readonly AreaControlDomain[],
|
||||
controls: AreaControlDomain[],
|
||||
areaId: string,
|
||||
excludeEntities: string[] | undefined,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -95,7 +95,6 @@ export interface TriggerList {
|
||||
|
||||
export interface BaseTrigger {
|
||||
alias?: string;
|
||||
note?: string;
|
||||
/** @deprecated Use `trigger` instead */
|
||||
platform?: string;
|
||||
trigger: string;
|
||||
@@ -241,7 +240,6 @@ export type Trigger = LegacyTrigger | TriggerList | PlatformTrigger;
|
||||
interface BaseCondition {
|
||||
condition: string;
|
||||
alias?: string;
|
||||
note?: string;
|
||||
enabled?: boolean;
|
||||
options?: Record<string, unknown>;
|
||||
}
|
||||
@@ -609,7 +607,6 @@ export interface AutomationClipboard {
|
||||
export interface BaseSidebarConfig {
|
||||
delete: () => void;
|
||||
close: (focus?: boolean) => void;
|
||||
editNote: () => void;
|
||||
}
|
||||
|
||||
export interface TriggerSidebarConfig extends BaseSidebarConfig {
|
||||
@@ -653,7 +650,6 @@ export interface ActionSidebarConfig extends BaseSidebarConfig {
|
||||
disable: () => void;
|
||||
continueOnError: () => void;
|
||||
duplicate: () => void;
|
||||
convert: () => void;
|
||||
cut: () => void;
|
||||
copy: () => void;
|
||||
insertAfter: (value: Action | Action[]) => boolean;
|
||||
@@ -672,7 +668,6 @@ export interface OptionSidebarConfig extends BaseSidebarConfig {
|
||||
rename: () => void;
|
||||
duplicate: () => void;
|
||||
defaultOption?: boolean;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface ScriptFieldSidebarConfig extends BaseSidebarConfig {
|
||||
|
||||
@@ -818,8 +818,7 @@ const describeLegacyTrigger = (
|
||||
if (trigger.trigger === "device" && trigger.device_id) {
|
||||
const config = trigger as DeviceTrigger;
|
||||
const localized = localizeDeviceAutomationTrigger(
|
||||
hass.localize,
|
||||
hass.states,
|
||||
hass,
|
||||
entityRegistry,
|
||||
config
|
||||
);
|
||||
@@ -1337,8 +1336,7 @@ const describeLegacyCondition = (
|
||||
if (condition.condition === "device" && condition.device_id) {
|
||||
const config = condition as DeviceCondition;
|
||||
const localized = localizeDeviceAutomationCondition(
|
||||
hass.localize,
|
||||
hass.states,
|
||||
hass,
|
||||
entityRegistry,
|
||||
config
|
||||
);
|
||||
|
||||
+2
-11
@@ -17,7 +17,6 @@ export interface BluetoothDeviceData extends DataTableRowData {
|
||||
source: string;
|
||||
time: number;
|
||||
tx_power: number;
|
||||
raw: string | null;
|
||||
}
|
||||
|
||||
export interface BluetoothConnectionData extends DataTableRowData {
|
||||
@@ -59,21 +58,13 @@ export interface BluetoothAllocationsData {
|
||||
allocated: string[];
|
||||
}
|
||||
|
||||
export type BluetoothScannerMode = "active" | "passive";
|
||||
|
||||
export type BluetoothScannerRequestedMode = BluetoothScannerMode | "auto";
|
||||
|
||||
export interface BluetoothScannerState {
|
||||
source: string;
|
||||
adapter: string;
|
||||
current_mode: BluetoothScannerMode | null;
|
||||
requested_mode: BluetoothScannerRequestedMode | null;
|
||||
current_mode: "active" | "passive" | null;
|
||||
requested_mode: "active" | "passive" | null;
|
||||
}
|
||||
|
||||
export const isScannerStateMismatch = (state: BluetoothScannerState): boolean =>
|
||||
state.requested_mode !== "auto" &&
|
||||
state.current_mode !== state.requested_mode;
|
||||
|
||||
export const subscribeBluetoothScannersDetailsUpdates = (
|
||||
conn: Connection,
|
||||
store: Store<BluetoothScannersDetails>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getColorByIndex } from "../common/color/colors";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { UNAVAILABLE } from "./entity/entity";
|
||||
import { isUnavailableState } from "./entity/entity";
|
||||
import type { EntityRegistryEntry } from "./entity/entity_registry";
|
||||
|
||||
export interface Calendar {
|
||||
@@ -120,7 +120,7 @@ export const getCalendars = (
|
||||
.filter(
|
||||
(eid) =>
|
||||
computeDomain(eid) === "calendar" &&
|
||||
hass.states[eid].state !== UNAVAILABLE &&
|
||||
!isUnavailableState(hass.states[eid].state) &&
|
||||
hass.entities[eid]?.hidden !== true
|
||||
)
|
||||
.sort()
|
||||
|
||||
@@ -40,6 +40,7 @@ export const createConfigFlow = (
|
||||
"config/config_entries/flow",
|
||||
{
|
||||
handler,
|
||||
show_advanced_options: Boolean(hass.userData?.showAdvanced),
|
||||
entry_id,
|
||||
},
|
||||
HEADERS
|
||||
|
||||
@@ -5,10 +5,7 @@ export interface DataTableFilter {
|
||||
|
||||
export type DataTableFilters = Record<string, DataTableFilter>;
|
||||
|
||||
export type DataTableFiltersValue =
|
||||
| string[]
|
||||
| Record<"key" | string, string[]>
|
||||
| undefined;
|
||||
export type DataTableFiltersValue = string[] | { key: string[] } | undefined;
|
||||
|
||||
export type DataTableFiltersValues = Record<string, DataTableFiltersValue>;
|
||||
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { HaFormSchema } from "../../components/ha-form/types";
|
||||
import type { CallWS } from "../../types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { BaseTrigger } from "../automation";
|
||||
import { migrateAutomationTrigger } from "../automation";
|
||||
import type { EntityRegistryEntry } from "../entity/entity_registry";
|
||||
import {
|
||||
computeEntityRegistryName,
|
||||
entityRegistryByEntityId,
|
||||
entityRegistryById,
|
||||
} from "../entity/entity_registry";
|
||||
|
||||
export interface DeviceAutomation {
|
||||
alias?: string;
|
||||
note?: string;
|
||||
device_id: string;
|
||||
domain: string;
|
||||
entity_id?: string;
|
||||
@@ -41,47 +39,49 @@ export interface DeviceCapabilities {
|
||||
extra_fields: HaFormSchema[];
|
||||
}
|
||||
|
||||
export const fetchDeviceActions = (callWS: CallWS, deviceId: string) =>
|
||||
callWS<DeviceAction[]>({
|
||||
export const fetchDeviceActions = (hass: HomeAssistant, deviceId: string) =>
|
||||
hass.callWS<DeviceAction[]>({
|
||||
type: "device_automation/action/list",
|
||||
device_id: deviceId,
|
||||
});
|
||||
|
||||
export const fetchDeviceConditions = (callWS: CallWS, deviceId: string) =>
|
||||
callWS<DeviceCondition[]>({
|
||||
export const fetchDeviceConditions = (hass: HomeAssistant, deviceId: string) =>
|
||||
hass.callWS<DeviceCondition[]>({
|
||||
type: "device_automation/condition/list",
|
||||
device_id: deviceId,
|
||||
});
|
||||
|
||||
export const fetchDeviceTriggers = (callWS: CallWS, deviceId: string) =>
|
||||
callWS<DeviceTrigger[]>({
|
||||
type: "device_automation/trigger/list",
|
||||
device_id: deviceId,
|
||||
}).then((triggers) => migrateAutomationTrigger(triggers) as DeviceTrigger[]);
|
||||
export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) =>
|
||||
hass
|
||||
.callWS<DeviceTrigger[]>({
|
||||
type: "device_automation/trigger/list",
|
||||
device_id: deviceId,
|
||||
})
|
||||
.then((triggers) => migrateAutomationTrigger(triggers) as DeviceTrigger[]);
|
||||
|
||||
export const fetchDeviceActionCapabilities = (
|
||||
callWS: CallWS,
|
||||
hass: HomeAssistant,
|
||||
action: DeviceAction
|
||||
) =>
|
||||
callWS<DeviceCapabilities>({
|
||||
hass.callWS<DeviceCapabilities>({
|
||||
type: "device_automation/action/capabilities",
|
||||
action,
|
||||
});
|
||||
|
||||
export const fetchDeviceConditionCapabilities = (
|
||||
callWS: CallWS,
|
||||
hass: HomeAssistant,
|
||||
condition: DeviceCondition
|
||||
) =>
|
||||
callWS<DeviceCapabilities>({
|
||||
hass.callWS<DeviceCapabilities>({
|
||||
type: "device_automation/condition/capabilities",
|
||||
condition,
|
||||
});
|
||||
|
||||
export const fetchDeviceTriggerCapabilities = (
|
||||
callWS: CallWS,
|
||||
hass: HomeAssistant,
|
||||
trigger: DeviceTrigger
|
||||
) =>
|
||||
callWS<DeviceCapabilities>({
|
||||
hass.callWS<DeviceCapabilities>({
|
||||
type: "device_automation/trigger/capabilities",
|
||||
trigger,
|
||||
});
|
||||
@@ -184,16 +184,19 @@ const compareEntityIdWithEntityRegId = (
|
||||
};
|
||||
|
||||
const getEntityName = (
|
||||
localize: LocalizeFunc,
|
||||
states: HassEntities,
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
entityId: string | undefined
|
||||
): string => {
|
||||
if (!entityId) {
|
||||
return `<${localize("ui.panel.config.automation.editor.unknown_entity")}>`;
|
||||
return (
|
||||
"<" +
|
||||
hass.localize("ui.panel.config.automation.editor.unknown_entity") +
|
||||
">"
|
||||
);
|
||||
}
|
||||
if (entityId.includes(".")) {
|
||||
const state = states[entityId];
|
||||
const state = hass.states[entityId];
|
||||
if (state) {
|
||||
return computeStateName(state);
|
||||
}
|
||||
@@ -201,35 +204,26 @@ const getEntityName = (
|
||||
}
|
||||
const entityReg = entityRegistryById(entityRegistry)[entityId];
|
||||
if (entityReg) {
|
||||
if (entityReg.name) {
|
||||
return entityReg.name;
|
||||
}
|
||||
const state = states[entityReg.entity_id];
|
||||
if (state) {
|
||||
return computeStateName(state);
|
||||
}
|
||||
return entityReg.original_name ?? entityId;
|
||||
return computeEntityRegistryName(hass, entityReg) || entityId;
|
||||
}
|
||||
return `<${localize("ui.panel.config.automation.editor.unknown_entity")}>`;
|
||||
return (
|
||||
"<" +
|
||||
hass.localize("ui.panel.config.automation.editor.unknown_entity") +
|
||||
">"
|
||||
);
|
||||
};
|
||||
|
||||
export const localizeDeviceAutomationAction = (
|
||||
localize: LocalizeFunc,
|
||||
states: HassEntities,
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
action: DeviceAction
|
||||
): string =>
|
||||
localize(
|
||||
hass.localize(
|
||||
`component.${action.domain}.device_automation.action_type.${action.type}`,
|
||||
{
|
||||
entity_name: getEntityName(
|
||||
localize,
|
||||
states,
|
||||
entityRegistry,
|
||||
action.entity_id
|
||||
),
|
||||
entity_name: getEntityName(hass, entityRegistry, action.entity_id),
|
||||
subtype: action.subtype
|
||||
? localize(
|
||||
? hass.localize(
|
||||
`component.${action.domain}.device_automation.action_subtype.${action.subtype}`
|
||||
) || action.subtype
|
||||
: "",
|
||||
@@ -237,22 +231,16 @@ export const localizeDeviceAutomationAction = (
|
||||
) || (action.subtype ? `"${action.subtype}" ${action.type}` : action.type!);
|
||||
|
||||
export const localizeDeviceAutomationCondition = (
|
||||
localize: LocalizeFunc,
|
||||
states: HassEntities,
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
condition: DeviceCondition
|
||||
): string =>
|
||||
localize(
|
||||
hass.localize(
|
||||
`component.${condition.domain}.device_automation.condition_type.${condition.type}`,
|
||||
{
|
||||
entity_name: getEntityName(
|
||||
localize,
|
||||
states,
|
||||
entityRegistry,
|
||||
condition.entity_id
|
||||
),
|
||||
entity_name: getEntityName(hass, entityRegistry, condition.entity_id),
|
||||
subtype: condition.subtype
|
||||
? localize(
|
||||
? hass.localize(
|
||||
`component.${condition.domain}.device_automation.condition_subtype.${condition.subtype}`
|
||||
) || condition.subtype
|
||||
: "",
|
||||
@@ -263,22 +251,16 @@ export const localizeDeviceAutomationCondition = (
|
||||
: condition.type!);
|
||||
|
||||
export const localizeDeviceAutomationTrigger = (
|
||||
localize: LocalizeFunc,
|
||||
states: HassEntities,
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
trigger: DeviceTrigger
|
||||
): string =>
|
||||
localize(
|
||||
hass.localize(
|
||||
`component.${trigger.domain}.device_automation.trigger_type.${trigger.type}`,
|
||||
{
|
||||
entity_name: getEntityName(
|
||||
localize,
|
||||
states,
|
||||
entityRegistry,
|
||||
trigger.entity_id
|
||||
),
|
||||
entity_name: getEntityName(hass, entityRegistry, trigger.entity_id),
|
||||
subtype: trigger.subtype
|
||||
? localize(
|
||||
? hass.localize(
|
||||
`component.${trigger.domain}.device_automation.trigger_subtype.${trigger.subtype}`
|
||||
) || trigger.subtype
|
||||
: "",
|
||||
@@ -287,18 +269,18 @@ export const localizeDeviceAutomationTrigger = (
|
||||
(trigger.subtype ? `"${trigger.subtype}" ${trigger.type}` : trigger.type!);
|
||||
|
||||
export const localizeExtraFieldsComputeLabelCallback =
|
||||
(localize: LocalizeFunc, deviceAutomation: DeviceAutomation) =>
|
||||
(hass: HomeAssistant, deviceAutomation: DeviceAutomation) =>
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
(schema): string =>
|
||||
localize(
|
||||
hass.localize(
|
||||
`component.${deviceAutomation.domain}.device_automation.extra_fields.${schema.name}`
|
||||
) || schema.name;
|
||||
|
||||
export const localizeExtraFieldsComputeHelperCallback =
|
||||
(localize: LocalizeFunc, deviceAutomation: DeviceAutomation) =>
|
||||
(hass: HomeAssistant, deviceAutomation: DeviceAutomation) =>
|
||||
// Returns a callback for ha-form to calculate helper texts per schema object
|
||||
(schema): string | undefined =>
|
||||
localize(
|
||||
hass.localize(
|
||||
`component.${deviceAutomation.domain}.device_automation.extra_fields_descriptions.${schema.name}`
|
||||
);
|
||||
|
||||
|
||||
@@ -2,23 +2,16 @@ import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { getDeviceArea } from "../../common/entity/context/get_device_context";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ConfigEntry } from "../config_entries";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../entity/entity";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "../entity/entity_registry";
|
||||
import { domainToName } from "../integration";
|
||||
import {
|
||||
getDeviceEntityDisplayLookup,
|
||||
type DeviceEntityDisplayLookup,
|
||||
type DeviceRegistryEntry,
|
||||
} from "./device_registry";
|
||||
|
||||
export interface DevicePickerItem extends PickerComboBoxItem {
|
||||
@@ -26,46 +19,6 @@ export interface DevicePickerItem extends PickerComboBoxItem {
|
||||
domain_name?: string;
|
||||
}
|
||||
|
||||
export interface DeviceAreaLabel {
|
||||
areaName?: string;
|
||||
viaDeviceName?: string;
|
||||
viaDeviceAreaName?: string;
|
||||
}
|
||||
|
||||
export const computeDeviceAreaLabel = (
|
||||
device: DeviceRegistryEntry,
|
||||
areas: HomeAssistant["areas"],
|
||||
devices: HomeAssistant["devices"],
|
||||
states: HomeAssistant["states"],
|
||||
localize: LocalizeFunc,
|
||||
language: HomeAssistant["language"],
|
||||
translationMetadata: HomeAssistant["translationMetadata"],
|
||||
viaDeviceEntities?: EntityRegistryEntry[] | EntityRegistryDisplayEntry[]
|
||||
): DeviceAreaLabel => {
|
||||
const area = getDeviceArea(device, areas);
|
||||
|
||||
const viaDevice = device.via_device_id
|
||||
? devices[device.via_device_id]
|
||||
: undefined;
|
||||
const viaDeviceName = viaDevice
|
||||
? computeDeviceNameDisplay(viaDevice, localize, states, viaDeviceEntities)
|
||||
: undefined;
|
||||
const viaDeviceArea = viaDevice ? getDeviceArea(viaDevice, areas) : undefined;
|
||||
const viaDeviceAreaName = viaDeviceArea
|
||||
? computeAreaName(viaDeviceArea)
|
||||
: undefined;
|
||||
|
||||
const isRTL = computeRTL(language, translationMetadata.translations);
|
||||
|
||||
const areaName = area
|
||||
? computeAreaName(area)
|
||||
: viaDeviceAreaName
|
||||
? `${viaDeviceAreaName}${isRTL ? " ◂ " : " ▸ "}${viaDeviceName}`
|
||||
: viaDeviceName || undefined;
|
||||
|
||||
return { areaName, viaDeviceName, viaDeviceAreaName };
|
||||
};
|
||||
|
||||
export const deviceComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "search_labels.deviceName",
|
||||
@@ -83,14 +36,6 @@ export const deviceComboBoxKeys: FuseWeightedKey[] = [
|
||||
name: "search_labels.domain",
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
name: "search_labels.viaDeviceName",
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
name: "search_labels.viaDeviceArea",
|
||||
weight: 3,
|
||||
},
|
||||
];
|
||||
|
||||
export const getDevices = (
|
||||
@@ -204,19 +149,9 @@ export const getDevices = (
|
||||
deviceEntityLookup[device.id]
|
||||
);
|
||||
|
||||
const { areaName, viaDeviceName, viaDeviceAreaName } =
|
||||
computeDeviceAreaLabel(
|
||||
device,
|
||||
hass.areas,
|
||||
hass.devices,
|
||||
hass.states,
|
||||
hass.localize,
|
||||
hass.language,
|
||||
hass.translationMetadata,
|
||||
device.via_device_id
|
||||
? deviceEntityLookup[device.via_device_id]
|
||||
: undefined
|
||||
);
|
||||
const area = getDeviceArea(device, hass.areas);
|
||||
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
|
||||
const configEntry = device.primary_config_entry
|
||||
? configEntryLookup?.[device.primary_config_entry]
|
||||
@@ -239,8 +174,6 @@ export const getDevices = (
|
||||
areaName: areaName || null,
|
||||
domain: domain || null,
|
||||
domainName: domainName || null,
|
||||
viaDeviceName: viaDeviceName || null,
|
||||
viaDeviceArea: viaDeviceAreaName || null,
|
||||
},
|
||||
sorting_label: [primary, areaName, domainName].filter(Boolean).join("_"),
|
||||
};
|
||||
|
||||
@@ -148,7 +148,6 @@ export interface GridSourceTypeEnergyPreference {
|
||||
power_config?: PowerConfig;
|
||||
|
||||
cost_adjustment_day: number;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface SolarSourceTypeEnergyPreference {
|
||||
@@ -157,7 +156,6 @@ export interface SolarSourceTypeEnergyPreference {
|
||||
stat_energy_from: string;
|
||||
stat_rate?: string;
|
||||
config_entry_solar_forecast: string[] | null;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface BatterySourceTypeEnergyPreference {
|
||||
@@ -167,7 +165,6 @@ export interface BatterySourceTypeEnergyPreference {
|
||||
stat_rate?: string; // always available if power_config is set
|
||||
power_config?: PowerConfig;
|
||||
stat_soc?: string;
|
||||
name?: string;
|
||||
}
|
||||
export interface GasSourceTypeEnergyPreference {
|
||||
type: "gas";
|
||||
|
||||
@@ -6,8 +6,10 @@ export const UNKNOWN = "unknown";
|
||||
export const ON = "on";
|
||||
export const OFF = "off";
|
||||
|
||||
export const UNAVAILABLE_STATES = [UNAVAILABLE, UNKNOWN] as const;
|
||||
export const OFF_STATES = [UNAVAILABLE, UNKNOWN, OFF] as const;
|
||||
|
||||
export const isUnavailableState = arrayLiteralIncludes(UNAVAILABLE_STATES);
|
||||
export const isOffState = arrayLiteralIncludes(OFF_STATES);
|
||||
|
||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||
|
||||
@@ -161,10 +161,6 @@ export interface VacuumEntityOptions {
|
||||
last_seen_segments?: Segment[];
|
||||
}
|
||||
|
||||
export interface DeviceTrackerEntityOptions {
|
||||
associated_zone?: string | null;
|
||||
}
|
||||
|
||||
export interface EntityRegistryOptions {
|
||||
number?: NumberEntityOptions;
|
||||
sensor?: SensorEntityOptions;
|
||||
@@ -176,7 +172,6 @@ export interface EntityRegistryOptions {
|
||||
cover?: CoverEntityOptions;
|
||||
valve?: ValveEntityOptions;
|
||||
vacuum?: VacuumEntityOptions;
|
||||
device_tracker?: DeviceTrackerEntityOptions;
|
||||
switch_as_x?: SwitchAsXEntityOptions;
|
||||
conversation?: Record<string, unknown>;
|
||||
"cloud.alexa"?: Record<string, unknown>;
|
||||
@@ -202,8 +197,7 @@ export interface EntityRegistryEntryUpdateParams {
|
||||
| LightEntityOptions
|
||||
| CoverEntityOptions
|
||||
| ValveEntityOptions
|
||||
| VacuumEntityOptions
|
||||
| DeviceTrackerEntityOptions;
|
||||
| VacuumEntityOptions;
|
||||
aliases?: (string | null)[];
|
||||
labels?: string[];
|
||||
categories?: Record<string, string | null>;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { ShortcutItem } from "./home_shortcuts";
|
||||
|
||||
export interface CoreFrontendUserData {
|
||||
showAdvanced?: boolean;
|
||||
showEntityIdPicker?: boolean;
|
||||
default_panel?: string;
|
||||
apps_info_dismissed?: boolean;
|
||||
|
||||
+199
-97
@@ -1,15 +1,11 @@
|
||||
import { atLeastVersion } from "../../common/config/version";
|
||||
import type { HaFormSchema } from "../../components/ha-form/types";
|
||||
import type {
|
||||
CallWS,
|
||||
HomeAssistant,
|
||||
HomeAssistantApi,
|
||||
TranslationDict,
|
||||
} from "../../types";
|
||||
import type { HomeAssistant, TranslationDict } from "../../types";
|
||||
import { supervisorApiCall } from "../supervisor/common";
|
||||
import type { StoreAddonDetails } from "../supervisor/store";
|
||||
import type { Supervisor, SupervisorArch } from "../supervisor/supervisor";
|
||||
import type { HassioResponse } from "./common";
|
||||
import { extractApiErrorMessage } from "./common";
|
||||
import { extractApiErrorMessage, hassioApiResultExtractor } from "./common";
|
||||
|
||||
export type AddonCapability = Exclude<
|
||||
keyof TranslationDict["ui"]["panel"]["config"]["apps"]["dashboard"]["capability"],
|
||||
@@ -147,38 +143,57 @@ export interface HassioAddonSetOptionParams {
|
||||
}
|
||||
|
||||
export const reloadHassioAddons = async (hass: HomeAssistant) => {
|
||||
await hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/addons/reload",
|
||||
method: "post",
|
||||
});
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
await hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/addons/reload",
|
||||
method: "post",
|
||||
});
|
||||
return;
|
||||
}
|
||||
await hass.callApi<HassioResponse<void>>("POST", `hassio/addons/reload`);
|
||||
};
|
||||
|
||||
export const fetchHassioAddonsInfo = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<HassioAddonsInfo> => {
|
||||
return hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/addons",
|
||||
method: "get",
|
||||
});
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
return hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/addons",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<HassioAddonsInfo>>("GET", `hassio/addons`)
|
||||
);
|
||||
};
|
||||
|
||||
export const fetchHassioAddonInfo = async (
|
||||
callWS: CallWS,
|
||||
hass: HomeAssistant,
|
||||
slug: string
|
||||
): Promise<HassioAddonDetails> => {
|
||||
return callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/info`,
|
||||
method: "get",
|
||||
});
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
return hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/info`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<HassioAddonDetails>>(
|
||||
"GET",
|
||||
`hassio/addons/${slug}/info`
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const fetchHassioAddonChangelog = async (
|
||||
api: HomeAssistantApi,
|
||||
hass: HomeAssistant,
|
||||
slug: string
|
||||
) => api.callApi<string>("GET", `hassio/addons/${slug}/changelog`);
|
||||
) => hass.callApi<string>("GET", `hassio/addons/${slug}/changelog`);
|
||||
|
||||
export const fetchHassioAddonLogs = async (hass: HomeAssistant, slug: string) =>
|
||||
hass.callApi<string>("GET", `hassio/addons/${slug}/logs`);
|
||||
@@ -189,77 +204,119 @@ export const fetchHassioAddonDocumentation = async (
|
||||
) => hass.callApi<string>("GET", `hassio/addons/${slug}/documentation`);
|
||||
|
||||
export const setHassioAddonOption = async (
|
||||
callWS: CallWS,
|
||||
hass: HomeAssistant,
|
||||
slug: string,
|
||||
data: HassioAddonSetOptionParams
|
||||
) => {
|
||||
const response = await callWS<HassioResponse<any>>({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/options`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
const response = await hass.callWS<HassioResponse<any>>({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/options`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
|
||||
if (response.result === "error") {
|
||||
throw Error(extractApiErrorMessage(response));
|
||||
if (response.result === "error") {
|
||||
throw Error(extractApiErrorMessage(response));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
return response;
|
||||
|
||||
return hass.callApi<HassioResponse<any>>(
|
||||
"POST",
|
||||
`hassio/addons/${slug}/options`,
|
||||
data
|
||||
);
|
||||
};
|
||||
|
||||
export const validateHassioAddonOption = async (
|
||||
callWS: CallWS,
|
||||
hass: HomeAssistant,
|
||||
slug: string,
|
||||
data?: any
|
||||
): Promise<{ message: string; valid: boolean }> => {
|
||||
return callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/options/validate`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
return hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/options/validate`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
await hass.callApi<HassioResponse<{ message: string; valid: boolean }>>(
|
||||
"POST",
|
||||
`hassio/addons/${slug}/options/validate`
|
||||
)
|
||||
).data;
|
||||
};
|
||||
|
||||
export const startHassioAddon = async (callWS: CallWS, slug: string) => {
|
||||
return callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/start`,
|
||||
method: "post",
|
||||
timeout: null,
|
||||
});
|
||||
export const startHassioAddon = async (hass: HomeAssistant, slug: string) => {
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
return hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/start`,
|
||||
method: "post",
|
||||
timeout: null,
|
||||
});
|
||||
}
|
||||
|
||||
return hass.callApi<string>("POST", `hassio/addons/${slug}/start`);
|
||||
};
|
||||
|
||||
export const stopHassioAddon = async (callWS: CallWS, slug: string) => {
|
||||
return callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/stop`,
|
||||
method: "post",
|
||||
timeout: null,
|
||||
});
|
||||
export const stopHassioAddon = async (hass: HomeAssistant, slug: string) => {
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
return hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/stop`,
|
||||
method: "post",
|
||||
timeout: null,
|
||||
});
|
||||
}
|
||||
|
||||
return hass.callApi<string>("POST", `hassio/addons/${slug}/stop`);
|
||||
};
|
||||
|
||||
export const setHassioAddonSecurity = async (
|
||||
callWS: CallWS,
|
||||
hass: HomeAssistant,
|
||||
slug: string,
|
||||
data: HassioAddonSetSecurityParams
|
||||
) => {
|
||||
await callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/security`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
await hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/security`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await hass.callApi<HassioResponse<void>>(
|
||||
"POST",
|
||||
`hassio/addons/${slug}/security`,
|
||||
data
|
||||
);
|
||||
};
|
||||
|
||||
export const installHassioAddon = async (
|
||||
callWS: CallWS,
|
||||
hass: HomeAssistant,
|
||||
slug: string
|
||||
): Promise<void> => {
|
||||
await callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/install`,
|
||||
method: "post",
|
||||
timeout: null,
|
||||
});
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
await hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/install`,
|
||||
method: "post",
|
||||
timeout: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await hass.callApi<HassioResponse<void>>(
|
||||
"POST",
|
||||
`hassio/addons/${slug}/install`
|
||||
);
|
||||
};
|
||||
|
||||
export const updateHassioAddon = async (
|
||||
@@ -267,37 +324,74 @@ export const updateHassioAddon = async (
|
||||
slug: string,
|
||||
backup: boolean
|
||||
): Promise<void> => {
|
||||
await hass.callWS({
|
||||
type: "hassio/update/addon",
|
||||
addon: slug,
|
||||
backup: backup,
|
||||
});
|
||||
if (atLeastVersion(hass.config.version, 2025, 2, 0)) {
|
||||
await hass.callWS({
|
||||
type: "hassio/update/addon",
|
||||
addon: slug,
|
||||
backup: backup,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
await hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/store/addons/${slug}/update`,
|
||||
method: "post",
|
||||
timeout: null,
|
||||
data: { backup },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await hass.callApi<HassioResponse<void>>(
|
||||
"POST",
|
||||
`hassio/addons/${slug}/update`,
|
||||
{ backup }
|
||||
);
|
||||
};
|
||||
|
||||
export const restartHassioAddon = async (
|
||||
callWS: CallWS,
|
||||
hass: HomeAssistant,
|
||||
slug: string
|
||||
): Promise<void> => {
|
||||
await callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/restart`,
|
||||
method: "post",
|
||||
timeout: null,
|
||||
});
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
await hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/restart`,
|
||||
method: "post",
|
||||
timeout: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await hass.callApi<HassioResponse<void>>(
|
||||
"POST",
|
||||
`hassio/addons/${slug}/restart`
|
||||
);
|
||||
};
|
||||
|
||||
export const uninstallHassioAddon = async (
|
||||
callWS: CallWS,
|
||||
hass: HomeAssistant,
|
||||
slug: string,
|
||||
removeData: boolean
|
||||
): Promise<void> => {
|
||||
await callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/uninstall`,
|
||||
method: "post",
|
||||
timeout: null,
|
||||
data: { remove_config: removeData },
|
||||
});
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
await hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/uninstall`,
|
||||
method: "post",
|
||||
timeout: null,
|
||||
data: { remove_config: removeData },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await hass.callApi<HassioResponse<void>>(
|
||||
"POST",
|
||||
`hassio/addons/${slug}/uninstall`,
|
||||
{ remove_config: removeData }
|
||||
);
|
||||
};
|
||||
|
||||
export const fetchAddonInfo = (
|
||||
@@ -313,13 +407,21 @@ export const fetchAddonInfo = (
|
||||
);
|
||||
|
||||
export const rebuildLocalAddon = async (
|
||||
callWS: CallWS,
|
||||
hass: HomeAssistant,
|
||||
slug: string
|
||||
): Promise<void> => {
|
||||
return callWS<undefined>({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/rebuild`,
|
||||
method: "post",
|
||||
timeout: null,
|
||||
});
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
return hass.callWS<undefined>({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/addons/${slug}/rebuild`,
|
||||
method: "post",
|
||||
timeout: null,
|
||||
});
|
||||
}
|
||||
return (
|
||||
await hass.callApi<HassioResponse<void>>(
|
||||
"POST",
|
||||
`hassio/addons/${slug}rebuild`
|
||||
)
|
||||
).data;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CallWS } from "../../types";
|
||||
import { atLeastVersion } from "../../common/config/version";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
export interface HassioResponse<T> {
|
||||
data: T;
|
||||
@@ -45,12 +46,21 @@ export const ignoreSupervisorError = (error): boolean => {
|
||||
};
|
||||
|
||||
export const fetchHassioStats = async (
|
||||
callWS: CallWS,
|
||||
hass: HomeAssistant,
|
||||
container: string
|
||||
): Promise<HassioStats> => {
|
||||
return callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/${container}/stats`,
|
||||
method: "get",
|
||||
});
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
return hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/${container}/stats`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<HassioStats>>(
|
||||
"GET",
|
||||
`hassio/${container}/stats`
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { LovelaceCardFeatureContext } from "../panels/lovelace/card-features/types";
|
||||
import type { LovelaceCardConfig } from "./lovelace/config/card";
|
||||
|
||||
export interface CustomCardSuggestion<
|
||||
T extends LovelaceCardConfig = LovelaceCardConfig,
|
||||
> {
|
||||
label?: string;
|
||||
config: T;
|
||||
}
|
||||
|
||||
export interface CustomCardEntry {
|
||||
type: string;
|
||||
@@ -16,10 +8,6 @@ export interface CustomCardEntry {
|
||||
description?: string;
|
||||
preview?: boolean;
|
||||
documentationURL?: string;
|
||||
getEntitySuggestion?: (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
) => CustomCardSuggestion | CustomCardSuggestion[] | null;
|
||||
}
|
||||
|
||||
export interface CustomBadgeEntry {
|
||||
|
||||
+5
-16
@@ -2,10 +2,7 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { navigate } from "../common/navigate";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import {
|
||||
subscribeDeviceRegistry,
|
||||
type DeviceRegistryEntry,
|
||||
} from "./device/device_registry";
|
||||
import { subscribeDeviceRegistry } from "./device/device_registry";
|
||||
import { getThreadDataSetTLV, listThreadDataSets } from "./thread";
|
||||
|
||||
export enum NetworkType {
|
||||
@@ -80,9 +77,9 @@ export const startExternalCommissioning = async (hass: HomeAssistant) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const watchForNewMatterDevice = (
|
||||
export const redirectOnNewMatterDevice = (
|
||||
hass: HomeAssistant,
|
||||
callback: (device: DeviceRegistryEntry) => void
|
||||
callback?: () => void
|
||||
): UnsubscribeFunc => {
|
||||
let curMatterDevices: Set<string> | undefined;
|
||||
const unsubDeviceReg = subscribeDeviceRegistry(hass.connection, (entries) => {
|
||||
@@ -104,7 +101,8 @@ export const watchForNewMatterDevice = (
|
||||
if (newMatterDevices.length) {
|
||||
unsubDeviceReg();
|
||||
curMatterDevices = undefined;
|
||||
callback(newMatterDevices[0]);
|
||||
callback?.();
|
||||
navigate(`/config/devices/device/${newMatterDevices[0].id}`);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
@@ -113,15 +111,6 @@ export const watchForNewMatterDevice = (
|
||||
};
|
||||
};
|
||||
|
||||
export const redirectOnNewMatterDevice = (
|
||||
hass: HomeAssistant,
|
||||
callback?: () => void
|
||||
): UnsubscribeFunc =>
|
||||
watchForNewMatterDevice(hass, (device) => {
|
||||
callback?.();
|
||||
navigate(`/config/devices/device/${device.id}`);
|
||||
});
|
||||
|
||||
export const addMatterDevice = (hass: HomeAssistant) => {
|
||||
startExternalCommissioning(hass);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
mdiPlaylistMusic,
|
||||
mdiPlayPause,
|
||||
mdiPodcast,
|
||||
mdiPowerStandby,
|
||||
mdiPower,
|
||||
mdiPowerOff,
|
||||
mdiPowerOn,
|
||||
mdiRepeat,
|
||||
@@ -295,7 +295,7 @@ export const computeMediaControls = (
|
||||
return supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_ON)
|
||||
? [
|
||||
{
|
||||
icon: mdiPowerStandby,
|
||||
icon: mdiPower,
|
||||
action: "turn_on",
|
||||
},
|
||||
]
|
||||
@@ -316,7 +316,7 @@ export const computeMediaControls = (
|
||||
|
||||
if (supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_OFF)) {
|
||||
buttons.push({
|
||||
icon: assumedState ? mdiPowerOff : mdiPowerStandby,
|
||||
icon: assumedState ? mdiPowerOff : mdiPower,
|
||||
action: "turn_off",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export const createOptionsFlow = (hass: HomeAssistant, handler: string) =>
|
||||
"config/config_entries/options/flow",
|
||||
{
|
||||
handler,
|
||||
show_advanced_options: Boolean(hass.userData?.showAdvanced),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
+1
-6
@@ -7,18 +7,13 @@ export interface GenericPreview {
|
||||
state: string;
|
||||
attributes: Record<string, any>;
|
||||
error?: string;
|
||||
domain?: string;
|
||||
}
|
||||
|
||||
export const subscribePreviewGeneric = (
|
||||
hass: HomeAssistant,
|
||||
domain: string,
|
||||
flow_id: string,
|
||||
flow_type:
|
||||
| "config_flow"
|
||||
| "options_flow"
|
||||
| "config_subentries_flow"
|
||||
| "repair_flow",
|
||||
flow_type: "config_flow" | "options_flow" | "config_subentries_flow",
|
||||
user_input: Record<string, any>,
|
||||
callback: (preview: GenericPreview) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
|
||||
@@ -36,7 +36,6 @@ export const isMaxMode = arrayLiteralIncludes(MODES_MAX);
|
||||
|
||||
export const baseActionStruct = object({
|
||||
alias: optional(string()),
|
||||
note: optional(string()),
|
||||
continue_on_error: optional(boolean()),
|
||||
enabled: optional(boolean()),
|
||||
});
|
||||
@@ -106,7 +105,6 @@ export interface Field {
|
||||
|
||||
interface BaseAction {
|
||||
alias?: string;
|
||||
note?: string;
|
||||
continue_on_error?: boolean;
|
||||
enabled?: boolean;
|
||||
}
|
||||
@@ -197,7 +195,6 @@ export interface ForEachRepeat extends BaseRepeat {
|
||||
|
||||
export interface Option {
|
||||
alias?: string;
|
||||
note?: string;
|
||||
conditions: string | Condition[];
|
||||
sequence: Action | Action[];
|
||||
}
|
||||
|
||||
@@ -335,8 +335,7 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
);
|
||||
}
|
||||
const localized = localizeDeviceAutomationAction(
|
||||
hass.localize,
|
||||
hass.states,
|
||||
hass,
|
||||
entityRegistry,
|
||||
config
|
||||
);
|
||||
|
||||
@@ -125,7 +125,7 @@ export interface BooleanSelector {
|
||||
boolean: {} | null;
|
||||
}
|
||||
|
||||
export type AutomationBehaviorTriggerMode = "first" | "all" | "each";
|
||||
export type AutomationBehaviorTriggerMode = "first" | "last" | "any";
|
||||
|
||||
export type AutomationBehaviorConditionMode = "all" | "any";
|
||||
|
||||
@@ -641,7 +641,7 @@ export const expandLabelTarget = (
|
||||
if (
|
||||
device.labels.includes(labelId) &&
|
||||
deviceMeetsTargetSelector(
|
||||
hass.states,
|
||||
hass,
|
||||
Object.values(entities),
|
||||
device,
|
||||
targetSelector,
|
||||
@@ -708,7 +708,7 @@ export const expandAreaTarget = (
|
||||
if (
|
||||
device.area_id === areaId &&
|
||||
deviceMeetsTargetSelector(
|
||||
hass.states,
|
||||
hass,
|
||||
Object.values(entities),
|
||||
device,
|
||||
targetSelector,
|
||||
@@ -768,7 +768,7 @@ export const areaMeetsTargetSelector = (
|
||||
if (
|
||||
device.area_id === areaId &&
|
||||
deviceMeetsTargetSelector(
|
||||
hass.states,
|
||||
hass,
|
||||
Object.values(entities),
|
||||
device,
|
||||
targetSelector,
|
||||
@@ -798,7 +798,7 @@ export const areaMeetsTargetSelector = (
|
||||
};
|
||||
|
||||
export const deviceMeetsTargetSelector = (
|
||||
states: HomeAssistant["states"],
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryDisplayEntry[] | EntityRegistryEntry[],
|
||||
device: DeviceRegistryEntry,
|
||||
targetSelector: TargetSelector,
|
||||
@@ -822,7 +822,7 @@ export const deviceMeetsTargetSelector = (
|
||||
(reg) => reg.device_id === device.id
|
||||
);
|
||||
return entities.some((entity) => {
|
||||
const entityState = states[entity.entity_id];
|
||||
const entityState = hass.states[entity.entity_id];
|
||||
return entityMeetsTargetSelector(
|
||||
entityState,
|
||||
targetSelector,
|
||||
|
||||
@@ -16,6 +16,7 @@ export const createSubConfigFlow = (
|
||||
"config/config_entries/subentries/flow",
|
||||
{
|
||||
handler: [configEntryId, subFlowType],
|
||||
show_advanced_options: Boolean(hass.userData?.showAdvanced),
|
||||
subentry_id,
|
||||
},
|
||||
HEADERS
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@ import { ensureArray } from "../common/array/ensure-array";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { CallWS, HomeAssistant } from "../types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { AreaRegistryEntry } from "./area/area_registry";
|
||||
import type { FloorComboBoxItem } from "./area_floor_picker";
|
||||
import type { DevicePickerItem } from "./device/device_picker";
|
||||
@@ -47,12 +47,12 @@ export interface ExtractFromTargetResultReferenced {
|
||||
}
|
||||
|
||||
export const extractFromTarget = async (
|
||||
callWS: CallWS,
|
||||
hass: HomeAssistant,
|
||||
target: HassServiceTarget,
|
||||
expandGroup = false,
|
||||
primaryEntitiesOnly = true
|
||||
) =>
|
||||
callWS<ExtractFromTargetResult>({
|
||||
hass.callWS<ExtractFromTargetResult>({
|
||||
type: "extract_from_target",
|
||||
target,
|
||||
expand_group: expandGroup,
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { HomeAssistant, ServiceCallResponse } from "../types";
|
||||
import { UNAVAILABLE } from "./entity/entity";
|
||||
import { isUnavailableState } from "./entity/entity";
|
||||
|
||||
export interface TodoList {
|
||||
entity_id: string;
|
||||
@@ -49,7 +49,7 @@ export const getTodoLists = (
|
||||
.filter(
|
||||
(entityId) =>
|
||||
computeDomain(entityId) === "todo" &&
|
||||
hass.states[entityId].state !== UNAVAILABLE &&
|
||||
!isUnavailableState(hass.states[entityId].state) &&
|
||||
(includeHidden || hass.entities[entityId]?.hidden !== true)
|
||||
)
|
||||
.map((entityId) => ({
|
||||
|
||||
+2
-9
@@ -29,13 +29,6 @@ import type {
|
||||
import type { SVGTemplateResult, TemplateResult } from "lit";
|
||||
import { css, html, svg } from "lit";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import {
|
||||
UNIT_HPA,
|
||||
UNIT_IN,
|
||||
UNIT_INHG,
|
||||
UNIT_KM,
|
||||
UNIT_MM,
|
||||
} from "../common/const";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { round } from "../common/number/round";
|
||||
import "../components/ha-svg-icon";
|
||||
@@ -252,12 +245,12 @@ export const getWeatherUnit = (
|
||||
case "precipitation":
|
||||
return (
|
||||
stateObj.attributes.precipitation_unit ||
|
||||
(lengthUnit === UNIT_KM ? UNIT_MM : UNIT_IN)
|
||||
(lengthUnit === "km" ? "mm" : "in")
|
||||
);
|
||||
case "pressure":
|
||||
return (
|
||||
stateObj.attributes.pressure_unit ||
|
||||
(lengthUnit === UNIT_KM ? UNIT_HPA : UNIT_INHG)
|
||||
(lengthUnit === "km" ? "hPa" : "inHg")
|
||||
);
|
||||
case "apparent_temperature":
|
||||
case "dew_point":
|
||||
|
||||
@@ -24,7 +24,6 @@ interface TemplatePreviewState {
|
||||
state: string;
|
||||
attributes: Record<string, any>;
|
||||
listeners: TemplateListeners;
|
||||
domain?: string;
|
||||
}
|
||||
|
||||
interface TemplatePreviewError {
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export type ZwaveCredentialType =
|
||||
| "pin_code"
|
||||
| "password"
|
||||
| "rfid_code"
|
||||
| "ble"
|
||||
| "nfc"
|
||||
| "uwb"
|
||||
| "eye_biometric"
|
||||
| "face_biometric"
|
||||
| "finger_biometric"
|
||||
| "hand_biometric"
|
||||
| "unspecified_biometric"
|
||||
| "desfire";
|
||||
|
||||
export const ENTERABLE_ZWAVE_CREDENTIAL_TYPES: readonly ZwaveCredentialType[] =
|
||||
["pin_code", "password"];
|
||||
|
||||
// UI surfaces only general + disposable to stay aligned with Matter lock UX.
|
||||
// Other types (programming, duress, non_access, remote_only, expiring) are
|
||||
// defined in translations for display in existing-user rows, but are not
|
||||
// selectable here.
|
||||
export const SIMPLE_USER_TYPES: readonly string[] = ["general", "disposable"];
|
||||
|
||||
// Fallback bounds when a lock advertises an enterable type without
|
||||
// per-type min/max — values mirror Z-Wave spec defaults.
|
||||
export const DEFAULT_CREDENTIAL_MIN_LENGTH = 4;
|
||||
export const DEFAULT_CREDENTIAL_MAX_LENGTH = 10;
|
||||
|
||||
export type CredentialErrorCode =
|
||||
| "required"
|
||||
| "length"
|
||||
| "pin_digits_only"
|
||||
| "";
|
||||
|
||||
export const enterableCredentialTypes = (
|
||||
capabilities: ZwaveCredentialCapabilities
|
||||
): ZwaveCredentialType[] => {
|
||||
if (!capabilities.supported_credential_types) {
|
||||
return [];
|
||||
}
|
||||
return ENTERABLE_ZWAVE_CREDENTIAL_TYPES.filter(
|
||||
(type) => type in capabilities.supported_credential_types
|
||||
);
|
||||
};
|
||||
|
||||
export const compatibleUserTypes = (
|
||||
capabilities: ZwaveCredentialCapabilities
|
||||
): string[] => {
|
||||
const supported = capabilities.supported_user_types ?? [];
|
||||
return SIMPLE_USER_TYPES.filter((t) => supported.includes(t));
|
||||
};
|
||||
|
||||
export const canAddZwaveUser = (
|
||||
capabilities: ZwaveCredentialCapabilities
|
||||
): boolean =>
|
||||
enterableCredentialTypes(capabilities).length > 0 &&
|
||||
compatibleUserTypes(capabilities).length > 0;
|
||||
|
||||
export const getCredentialError = (
|
||||
data: string,
|
||||
type: ZwaveCredentialType | "",
|
||||
capability: ZwaveCredentialTypeCapability | undefined
|
||||
): CredentialErrorCode => {
|
||||
if (!data) {
|
||||
return "required";
|
||||
}
|
||||
const minLength = capability?.min_length ?? DEFAULT_CREDENTIAL_MIN_LENGTH;
|
||||
const maxLength = capability?.max_length ?? DEFAULT_CREDENTIAL_MAX_LENGTH;
|
||||
if (data.length < minLength || data.length > maxLength) {
|
||||
return "length";
|
||||
}
|
||||
if (type === "pin_code" && !/^\d+$/.test(data)) {
|
||||
return "pin_digits_only";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
export interface ZwaveCredentialTypeCapability {
|
||||
num_slots: number;
|
||||
min_length: number;
|
||||
max_length: number;
|
||||
supports_learn: boolean;
|
||||
}
|
||||
|
||||
export interface ZwaveCredentialCapabilities {
|
||||
supports_user_management: boolean;
|
||||
max_users: number;
|
||||
supported_user_types: string[];
|
||||
max_user_name_length: number;
|
||||
supported_credential_rules: string[];
|
||||
supported_credential_types: Partial<
|
||||
Record<ZwaveCredentialType, ZwaveCredentialTypeCapability>
|
||||
>;
|
||||
}
|
||||
|
||||
export interface ZwaveCredential {
|
||||
type: ZwaveCredentialType;
|
||||
slot: number;
|
||||
}
|
||||
|
||||
export interface ZwaveUser {
|
||||
user_id: number;
|
||||
user_name: string | null;
|
||||
active: boolean;
|
||||
user_type: string;
|
||||
credential_rule: string | null;
|
||||
credentials: ZwaveCredential[];
|
||||
}
|
||||
|
||||
export interface ZwaveUsersResponse {
|
||||
max_users: number;
|
||||
users: ZwaveUser[];
|
||||
}
|
||||
|
||||
export interface SetZwaveUserParams {
|
||||
user_id?: number;
|
||||
user_name?: string | null;
|
||||
user_type?: string;
|
||||
credential_rule?: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface SetZwaveUserResult {
|
||||
user_id: number;
|
||||
}
|
||||
|
||||
export interface SetZwaveCredentialParams {
|
||||
user_id: number;
|
||||
credential_type: ZwaveCredentialType;
|
||||
credential_data: string;
|
||||
credential_slot?: number;
|
||||
}
|
||||
|
||||
export interface SetZwaveCredentialResult {
|
||||
credential_slot: number;
|
||||
user_id: number;
|
||||
}
|
||||
|
||||
export interface DeleteZwaveCredentialParams {
|
||||
user_id: number;
|
||||
credential_type: ZwaveCredentialType;
|
||||
credential_slot: number;
|
||||
}
|
||||
|
||||
// The Z-Wave services key their response by entity_id to support multi-target
|
||||
// calls. The frontend only ever calls them with a single lock entity, so we
|
||||
// expect exactly that key. Anything else (no response, mismatched key) is a
|
||||
// backend contract violation — surface it as a localized error rather than
|
||||
// letting `cannot read property of undefined` bubble up.
|
||||
const unwrapEntityResponse = <T>(
|
||||
hass: HomeAssistant,
|
||||
response: Record<string, T> | undefined,
|
||||
entity_id: string
|
||||
): T => {
|
||||
const value = response?.[entity_id];
|
||||
if (value === undefined) {
|
||||
throw new Error(
|
||||
hass.localize(
|
||||
"ui.panel.config.zwave_js.credentials.errors.empty_response"
|
||||
)
|
||||
);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const callCredentialService = async <T>(
|
||||
hass: HomeAssistant,
|
||||
service: string,
|
||||
entity_id: string,
|
||||
params: Record<string, unknown> = {}
|
||||
): Promise<T> => {
|
||||
// notifyOnError=false — callers surface errors in-dialog instead.
|
||||
const result = await hass.callService<Record<string, T>>(
|
||||
"zwave_js",
|
||||
service,
|
||||
params,
|
||||
{ entity_id },
|
||||
false,
|
||||
true
|
||||
);
|
||||
return unwrapEntityResponse(hass, result.response, entity_id);
|
||||
};
|
||||
|
||||
export const getZwaveCredentialCapabilities = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string
|
||||
): Promise<ZwaveCredentialCapabilities> =>
|
||||
callCredentialService<ZwaveCredentialCapabilities>(
|
||||
hass,
|
||||
"get_credential_capabilities",
|
||||
entity_id
|
||||
);
|
||||
|
||||
export const getZwaveUsers = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string
|
||||
): Promise<ZwaveUsersResponse> =>
|
||||
callCredentialService<ZwaveUsersResponse>(hass, "get_users", entity_id);
|
||||
|
||||
export const setZwaveUser = async (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
params: SetZwaveUserParams
|
||||
): Promise<SetZwaveUserResult> => {
|
||||
// notifyOnError=false — caller surfaces errors in-dialog instead.
|
||||
const result = await hass.callService<Record<string, SetZwaveUserResult>>(
|
||||
"zwave_js",
|
||||
"set_user",
|
||||
params,
|
||||
{ entity_id },
|
||||
false,
|
||||
true
|
||||
);
|
||||
return unwrapEntityResponse(hass, result.response, entity_id);
|
||||
};
|
||||
|
||||
export const deleteZwaveUser = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
user_id: number
|
||||
) =>
|
||||
hass.callService(
|
||||
"zwave_js",
|
||||
"delete_user",
|
||||
{ user_id },
|
||||
{ entity_id },
|
||||
false
|
||||
);
|
||||
|
||||
export const deleteZwaveAllUsers = (hass: HomeAssistant, entity_id: string) =>
|
||||
hass.callService("zwave_js", "delete_all_users", {}, { entity_id }, false);
|
||||
|
||||
export const setZwaveCredential = async (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
params: SetZwaveCredentialParams
|
||||
): Promise<SetZwaveCredentialResult> => {
|
||||
// notifyOnError=false — caller surfaces errors in-dialog instead.
|
||||
const result = await hass.callService<
|
||||
Record<string, SetZwaveCredentialResult>
|
||||
>("zwave_js", "set_credential", params, { entity_id }, false, true);
|
||||
return unwrapEntityResponse(hass, result.response, entity_id);
|
||||
};
|
||||
|
||||
export const deleteZwaveCredential = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
params: DeleteZwaveCredentialParams
|
||||
) =>
|
||||
hass.callService(
|
||||
"zwave_js",
|
||||
"delete_credential",
|
||||
params,
|
||||
{ entity_id },
|
||||
false
|
||||
);
|
||||
@@ -18,7 +18,7 @@ import "../../../components/ha-slider";
|
||||
import "../../../components/ha-time-input";
|
||||
import "../../../components/input/ha-input";
|
||||
import { isTiltOnly } from "../../../data/cover";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { ImageEntity } from "../../../data/image";
|
||||
import { computeImageUrl } from "../../../data/image";
|
||||
import "../../../panels/lovelace/components/hui-timestamp-display";
|
||||
@@ -108,13 +108,14 @@ class EntityPreviewRow extends LitElement {
|
||||
|
||||
private _renderEntityState(stateObj: HassEntity): TemplateResult | string {
|
||||
const domain = stateObj.entity_id.split(".", 1)[0];
|
||||
const disabled = stateObj.state === UNAVAILABLE;
|
||||
const noValue =
|
||||
stateObj.state === UNAVAILABLE || stateObj.state === UNKNOWN;
|
||||
|
||||
if (domain === "button") {
|
||||
return html`
|
||||
<ha-button appearance="plain" size="small" .disabled=${disabled}>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
>
|
||||
${this.hass.localize("ui.card.button.press")}
|
||||
</ha-button>
|
||||
`;
|
||||
@@ -150,15 +151,19 @@ class EntityPreviewRow extends LitElement {
|
||||
return html`
|
||||
<ha-date-input
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${disabled}
|
||||
.value=${noValue ? undefined : stateObj.state}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.value=${isUnavailableState(stateObj.state)
|
||||
? undefined
|
||||
: stateObj.state}
|
||||
>
|
||||
</ha-date-input>
|
||||
`;
|
||||
}
|
||||
|
||||
if (domain === "datetime") {
|
||||
const dateObj = noValue ? undefined : new Date(stateObj.state);
|
||||
const dateObj = isUnavailableState(stateObj.state)
|
||||
? undefined
|
||||
: new Date(stateObj.state);
|
||||
const time = dateObj ? format(dateObj, "HH:mm:ss") : undefined;
|
||||
const date = dateObj ? format(dateObj, "yyyy-MM-dd") : undefined;
|
||||
return html`
|
||||
@@ -167,12 +172,12 @@ class EntityPreviewRow extends LitElement {
|
||||
.label=${computeStateName(stateObj)}
|
||||
.locale=${this.hass.locale}
|
||||
.value=${date}
|
||||
.disabled=${disabled}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
>
|
||||
</ha-date-input>
|
||||
<ha-time-input
|
||||
.value=${time}
|
||||
.disabled=${disabled}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.locale=${this.hass.locale}
|
||||
></ha-time-input>
|
||||
</div>
|
||||
@@ -182,7 +187,7 @@ class EntityPreviewRow extends LitElement {
|
||||
if (domain === "event") {
|
||||
return html`
|
||||
<div class="when">
|
||||
${noValue
|
||||
${isUnavailableState(stateObj.state)
|
||||
? this.hass.formatEntityState(stateObj)
|
||||
: html`<hui-timestamp-display
|
||||
.hass=${this.hass}
|
||||
@@ -191,7 +196,7 @@ class EntityPreviewRow extends LitElement {
|
||||
></hui-timestamp-display>`}
|
||||
</div>
|
||||
<div class="what">
|
||||
${noValue
|
||||
${isUnavailableState(stateObj.state)
|
||||
? nothing
|
||||
: this.hass.formatEntityAttributeValue(stateObj, "event_type")}
|
||||
</div>
|
||||
@@ -201,7 +206,9 @@ class EntityPreviewRow extends LitElement {
|
||||
const toggleDomains = ["fan", "light", "remote", "siren", "switch"];
|
||||
if (toggleDomains.includes(domain)) {
|
||||
const showToggle =
|
||||
stateObj.state === "on" || stateObj.state === "off" || noValue;
|
||||
stateObj.state === "on" ||
|
||||
stateObj.state === "off" ||
|
||||
isUnavailableState(stateObj.state);
|
||||
return html`
|
||||
${showToggle
|
||||
? html`
|
||||
@@ -234,7 +241,7 @@ class EntityPreviewRow extends LitElement {
|
||||
if (domain === "lock") {
|
||||
return html`
|
||||
<ha-button
|
||||
.disabled=${disabled}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
class="text-content"
|
||||
appearance="plain"
|
||||
size="small"
|
||||
@@ -259,7 +266,7 @@ class EntityPreviewRow extends LitElement {
|
||||
<div class="numberflex">
|
||||
<ha-slider
|
||||
labeled
|
||||
.disabled=${disabled}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
.step=${Number(stateObj.attributes.step)}
|
||||
.min=${Number(stateObj.attributes.min)}
|
||||
.max=${Number(stateObj.attributes.max)}
|
||||
@@ -273,7 +280,7 @@ class EntityPreviewRow extends LitElement {
|
||||
: html`<div class="numberflex numberstate">
|
||||
<ha-input
|
||||
auto-validate
|
||||
.disabled=${disabled}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
pattern="[0-9]+([\\.][0-9]+)?"
|
||||
.step=${Number(stateObj.attributes.step)}
|
||||
.min=${Number(stateObj.attributes.min)}
|
||||
@@ -296,7 +303,7 @@ class EntityPreviewRow extends LitElement {
|
||||
<ha-select
|
||||
.label=${computeStateName(stateObj)}
|
||||
.value=${stateObj.state}
|
||||
.disabled=${disabled}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
.options=${stateObj.attributes.options?.map((option) => ({
|
||||
value: option,
|
||||
label: this.hass!.formatEntityState(stateObj, option),
|
||||
@@ -310,7 +317,7 @@ class EntityPreviewRow extends LitElement {
|
||||
const showSensor =
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
|
||||
stateObj.attributes.device_class
|
||||
) && !noValue;
|
||||
) && !isUnavailableState(stateObj.state);
|
||||
return html`
|
||||
${showSensor
|
||||
? html`
|
||||
@@ -332,7 +339,7 @@ class EntityPreviewRow extends LitElement {
|
||||
return html`
|
||||
<ha-input
|
||||
.label=${computeStateName(stateObj)}
|
||||
.disabled=${disabled}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.value=${stateObj.state}
|
||||
.minlength=${stateObj.attributes.min}
|
||||
.maxlength=${stateObj.attributes.max}
|
||||
@@ -347,9 +354,11 @@ class EntityPreviewRow extends LitElement {
|
||||
if (domain === "time") {
|
||||
return html`
|
||||
<ha-time-input
|
||||
.value=${noValue ? undefined : stateObj.state}
|
||||
.value=${isUnavailableState(stateObj.state)
|
||||
? undefined
|
||||
: stateObj.state}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${disabled}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
></ha-time-input>
|
||||
`;
|
||||
}
|
||||
@@ -357,7 +366,7 @@ class EntityPreviewRow extends LitElement {
|
||||
if (domain === "weather") {
|
||||
return html`
|
||||
<div>
|
||||
${noValue ||
|
||||
${isUnavailableState(stateObj.state) ||
|
||||
stateObj.attributes.temperature === undefined ||
|
||||
stateObj.attributes.temperature === null
|
||||
? this.hass.formatEntityState(stateObj)
|
||||
|
||||
@@ -65,7 +65,7 @@ export class FlowPreviewGeneric extends LitElement {
|
||||
}
|
||||
const now = new Date().toISOString();
|
||||
this._preview = {
|
||||
entity_id: `${preview.domain ?? this.stepId}.___flow_preview___`,
|
||||
entity_id: `${this.stepId}.___flow_preview___`,
|
||||
last_changed: now,
|
||||
last_updated: now,
|
||||
context: { id: "", parent_id: null, user_id: null },
|
||||
@@ -85,8 +85,7 @@ export class FlowPreviewGeneric extends LitElement {
|
||||
if (
|
||||
this.flowType !== "config_flow" &&
|
||||
this.flowType !== "options_flow" &&
|
||||
this.flowType !== "config_subentries_flow" &&
|
||||
this.flowType !== "repair_flow"
|
||||
this.flowType !== "config_subentries_flow"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ class FlowPreviewTemplate extends LitElement {
|
||||
this._listeners = preview.listeners;
|
||||
const now = new Date().toISOString();
|
||||
this._preview = {
|
||||
entity_id: `${preview.domain ?? this.stepId}.___flow_preview___`,
|
||||
entity_id: `${this.stepId}.___flow_preview___`,
|
||||
last_changed: now,
|
||||
last_updated: now,
|
||||
context: { id: "", parent_id: null, user_id: null },
|
||||
|
||||
@@ -167,6 +167,7 @@ export interface DataEntryFlowDialogParams {
|
||||
entryId?: string;
|
||||
}) => void;
|
||||
flowConfig: FlowConfig;
|
||||
showAdvanced?: boolean;
|
||||
dialogParentElement?: HTMLElement;
|
||||
navigateToResult?: boolean;
|
||||
carryOverDevices?: string[];
|
||||
|
||||
@@ -48,6 +48,7 @@ class StepFlowAbort extends LitElement {
|
||||
showConfigFlowDialog(this.params.dialogParentElement!, {
|
||||
dialogClosedCallback: this.params.dialogClosedCallback,
|
||||
startFlowHandler: this.handler,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
navigateToResult: this.params.navigateToResult,
|
||||
});
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user