mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-17 05:09:25 +00:00
Compare commits
61 Commits
20230904.0
...
delay-init
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7f2fcc73b5 | ||
![]() |
4b5c7021ff | ||
![]() |
3349031cbd | ||
![]() |
5e107d43d7 | ||
![]() |
e46f2cd9bf | ||
![]() |
713ebfcc22 | ||
![]() |
46e4eafe95 | ||
![]() |
e6fd18e23b | ||
![]() |
71cd71dfd5 | ||
![]() |
1019ccfd26 | ||
![]() |
577c1d8522 | ||
![]() |
63f0b469cc | ||
![]() |
e688417863 | ||
![]() |
a19633e2d4 | ||
![]() |
8797142cca | ||
![]() |
2a7403b6fd | ||
![]() |
22efe14149 | ||
![]() |
7cce24bcd1 | ||
![]() |
b8f0bb66cd | ||
![]() |
b950f990b4 | ||
![]() |
b511e7a37d | ||
![]() |
50f4b78f2e | ||
![]() |
7b0b4cdfe4 | ||
![]() |
c60e5c4c61 | ||
![]() |
709a63e6da | ||
![]() |
f689eed073 | ||
![]() |
cd55eee2fc | ||
![]() |
cf27e68748 | ||
![]() |
472ed2fe82 | ||
![]() |
d0a60984ed | ||
![]() |
24d401061c | ||
![]() |
2352d05573 | ||
![]() |
87d53e38c4 | ||
![]() |
db3c535884 | ||
![]() |
158b24f902 | ||
![]() |
19c4ed4690 | ||
![]() |
eae4ca1271 | ||
![]() |
0276430ab5 | ||
![]() |
db7caf1c32 | ||
![]() |
7176a51fec | ||
![]() |
4a6539d75b | ||
![]() |
850699ea70 | ||
![]() |
c17cc22f88 | ||
![]() |
9e3f2d5cb7 | ||
![]() |
0677c9c7b0 | ||
![]() |
af7e385884 | ||
![]() |
ba88fef09b | ||
![]() |
ad0e59c8f4 | ||
![]() |
14e6f5e8ca | ||
![]() |
3c48157793 | ||
![]() |
3a07af6ad2 | ||
![]() |
c1c05f8d22 | ||
![]() |
29aed5371c | ||
![]() |
76c878df57 | ||
![]() |
d6e7ebe71d | ||
![]() |
085b26d5ea | ||
![]() |
32472ca627 | ||
![]() |
c3c4bb4421 | ||
![]() |
f7f1a0c32d | ||
![]() |
d4872b177f | ||
![]() |
5bb8c51d25 |
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v4.0.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v4.0.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v4.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.3.2
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v4.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v4.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v4.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
with:
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v4.0.0
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v4.0.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v4.0.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v4.0.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v4.0.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.8.1
|
||||
|
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v4.0.0
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v4
|
||||
|
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v4.0.0
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v4.0.0
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
@@ -100,6 +100,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
||||
useBuiltIns: latestBuild ? false : "entry",
|
||||
corejs: latestBuild ? false : { version: "3.32", proposals: true },
|
||||
bugfixes: true,
|
||||
shippedProposals: true,
|
||||
},
|
||||
],
|
||||
"@babel/preset-typescript",
|
||||
|
@@ -1,10 +1,14 @@
|
||||
import fs from "fs/promises";
|
||||
import gulp from "gulp";
|
||||
import path from "path";
|
||||
import mapStream from "map-stream";
|
||||
import transform from "gulp-json-transform";
|
||||
import { LokaliseApi } from "@lokalise/node-api";
|
||||
import JSZip from "jszip";
|
||||
|
||||
const inDirFrontend = "translations/frontend";
|
||||
const inDirBackend = "translations/backend";
|
||||
const inDir = "translations";
|
||||
const inDirFrontend = `${inDir}/frontend`;
|
||||
const inDirBackend = `${inDir}/backend`;
|
||||
const srcMeta = "src/translations/translationMetadata.json";
|
||||
const encoding = "utf8";
|
||||
|
||||
@@ -68,8 +72,9 @@ gulp.task("convert-backend-translations", function () {
|
||||
});
|
||||
|
||||
gulp.task("check-translations-html", function () {
|
||||
// We exclude backend translations because they are not compliant with the HTML rule for now
|
||||
return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml());
|
||||
return gulp
|
||||
.src([`${inDirFrontend}/*.json`, `${inDirBackend}/*.json`])
|
||||
.pipe(checkHtml());
|
||||
});
|
||||
|
||||
gulp.task("check-all-files-exist", async function () {
|
||||
@@ -89,7 +94,83 @@ gulp.task("check-all-files-exist", async function () {
|
||||
await Promise.allSettled(writings);
|
||||
});
|
||||
|
||||
const lokaliseProjects = {
|
||||
backend: "130246255a974bd3b5e8a1.51616605",
|
||||
frontend: "3420425759f6d6d241f598.13594006",
|
||||
};
|
||||
|
||||
gulp.task("fetch-lokalise", async function () {
|
||||
let apiKey;
|
||||
try {
|
||||
apiKey =
|
||||
process.env.LOKALISE_TOKEN ||
|
||||
(await fs.readFile(".lokalise_token", { encoding }));
|
||||
} catch {
|
||||
throw new Error(
|
||||
"An Administrator Lokalise API token is required to download the latest set of translations. Place your token in a new file `.lokalise_token` in the repo root directory."
|
||||
);
|
||||
}
|
||||
const lokaliseApi = new LokaliseApi({ apiKey });
|
||||
|
||||
const mkdirPromise = Promise.all([
|
||||
fs.mkdir(inDirFrontend, { recursive: true }),
|
||||
fs.mkdir(inDirBackend, { recursive: true }),
|
||||
]);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(lokaliseProjects).map(([project, projectId]) =>
|
||||
lokaliseApi
|
||||
.files()
|
||||
.download(projectId, {
|
||||
format: "json",
|
||||
original_filenames: false,
|
||||
replace_breaks: false,
|
||||
json_unescaped_slashes: true,
|
||||
export_empty_as: "skip",
|
||||
})
|
||||
.then((download) => fetch(download.bundle_url))
|
||||
.then((response) => {
|
||||
if (response.status === 200 || response.status === 0) {
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
throw new Error(response.statusText);
|
||||
})
|
||||
.then(JSZip.loadAsync)
|
||||
.then(async (contents) => {
|
||||
await mkdirPromise;
|
||||
return Promise.all(
|
||||
Object.keys(contents.files).map(async (filename) => {
|
||||
const file = contents.file(filename);
|
||||
if (!file) {
|
||||
// no file, probably a directory
|
||||
return Promise.resolve();
|
||||
}
|
||||
return file
|
||||
.async("nodebuffer")
|
||||
.then((content) =>
|
||||
fs.writeFile(
|
||||
path.join(
|
||||
inDir,
|
||||
project,
|
||||
filename.split("/").splice(-1)[0]
|
||||
),
|
||||
content,
|
||||
{ flag: "w", encoding }
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"check-downloaded-translations",
|
||||
gulp.series("check-translations-html", "check-all-files-exist")
|
||||
"download-translations",
|
||||
gulp.series(
|
||||
"fetch-lokalise",
|
||||
"convert-backend-translations",
|
||||
"check-translations-html",
|
||||
"check-all-files-exist"
|
||||
)
|
||||
);
|
||||
|
@@ -6,6 +6,8 @@ import presetEnv from "@babel/preset-env";
|
||||
import compilationTargets from "@babel/helper-compilation-targets";
|
||||
import coreJSCompat from "core-js-compat";
|
||||
import { logPlugin } from "@babel/preset-env/lib/debug.js";
|
||||
// eslint-disable-next-line import/no-relative-packages
|
||||
import shippedPolyfills from "../node_modules/babel-plugin-polyfill-corejs3/lib/shipped-proposals.js";
|
||||
import { babelOptions } from "./bundle.cjs";
|
||||
|
||||
const detailsOpen = (heading) =>
|
||||
@@ -26,6 +28,22 @@ const dummyAPI = {
|
||||
targets: () => ({}),
|
||||
};
|
||||
|
||||
// Generate filter function based on proposal/method inputs
|
||||
// Copied and adapted from babel-plugin-polyfill-corejs3/esm/index.mjs
|
||||
const polyfillFilter = (method, proposals, shippedProposals) => (name) => {
|
||||
if (proposals || method === "entry-global") return true;
|
||||
if (shippedProposals && shippedPolyfills.default.has(name)) {
|
||||
return true;
|
||||
}
|
||||
if (name.startsWith("esnext.")) {
|
||||
const esName = `es.${name.slice(7)}`;
|
||||
// If its imaginative esName is not in latest compat data, it means the proposal is not stage 4
|
||||
return esName in coreJSCompat.data;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Log the plugins and polyfills for each build environment
|
||||
for (const buildType of ["Modern", "Legacy"]) {
|
||||
const browserslistEnv = buildType.toLowerCase();
|
||||
const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" });
|
||||
@@ -46,7 +64,13 @@ for (const buildType of ["Modern", "Legacy"]) {
|
||||
const targets = compilationTargets.default(babelOpts?.targets, {
|
||||
browserslistEnv,
|
||||
});
|
||||
const polyfillList = coreJSCompat({ targets }).list;
|
||||
const polyfillList = coreJSCompat({ targets }).list.filter(
|
||||
polyfillFilter(
|
||||
`${presetEnvOpts.useBuiltIns}-global`,
|
||||
presetEnvOpts?.corejs?.proposals,
|
||||
presetEnvOpts?.shippedProposals
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
"The following %i polyfills may be injected by Babel:\n",
|
||||
polyfillList.length
|
||||
|
File diff suppressed because one or more lines are too long
4
gallery/public/images/brand/README.md
Normal file
4
gallery/public/images/brand/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Note!
|
||||
|
||||
Note, the assets in this folder, are not part of the CC license this repository is shipped in.
|
||||
All rights reserved.
|
BIN
gallery/public/images/brand/logo-exclusion-zone.png
Normal file
BIN
gallery/public/images/brand/logo-exclusion-zone.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
BIN
gallery/public/images/brand/logo-layout-variants.png
Normal file
BIN
gallery/public/images/brand/logo-layout-variants.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
BIN
gallery/public/images/brand/logo.png
Normal file
BIN
gallery/public/images/brand/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
Before Width: | Height: | Size: 22 KiB |
@@ -2,30 +2,86 @@
|
||||
title: "Logo"
|
||||
---
|
||||
|
||||
# Using our logo
|
||||
# Our logo
|
||||
|
||||
As a community, we are proud of our logo. Follow these guidelines to ensure it always looks its best. Our logo follows Google's material design spec and uses the blue interface color.
|
||||
As a community, we are proud of our logo. Follow these guidelines to ensure it always represents the identity of the Home Assistant project and community the best way possible.
|
||||
|
||||
[Download Logo](https://github.com/home-assistant/assets/tree/master/logo)
|
||||
|
||||

|
||||

|
||||
|
||||
## Using the icon
|
||||
Please note that this logo is not released under the CC license. All rights reserved.
|
||||
|
||||
Our icon is a shorter and most used version of our logo. The icon can exist without the wordmark, the wordmark should never exist without the icon.
|
||||
# Design
|
||||
|
||||

|
||||
At the core of the Home Assistant logomark is the Blue House with Antenna, the three most recognizable and distinct features of the previous logo throughout the past decade.
|
||||
|
||||
## Using the right variant
|
||||
### Blue
|
||||
|
||||
The pretty blue logo with a background shadow, pictured top left, is our primary logo. It should only be used with black, white, and non-duotone photography.
|
||||
Blue feels stable and essential. A bright sky blue is joyful, clear, and free of clouds.
|
||||
|
||||
When needed you can use our logo without a shadow, as seen as the second variant.
|
||||
### House
|
||||
|
||||
The outlined logo should only be used on packaging.
|
||||
Of all possible combinations of shapes, a home is best abstracted in the shape of a structure with a pitched roof. With the vast amount of logos based on this shape, the best we can do is to make it more iconic. The house is further simplified - there is no gable and there is no chimney - to an orthogonal shape with an elegant and deliberate proportion.
|
||||
|
||||
## Exclusion zone
|
||||
### Antenna
|
||||
|
||||
The logo needs some personal space. It's exclusion zone is equal to a quarter the height of the icon.
|
||||
Call it a tree, a set of nodes, a PCB, or an antenna. The antenna is the most recognizable and memorable part of the previous Home Assistant logo, and is an easily understandable symbol that conveys technologies that are smart, connected, and growing evergreen.
|
||||
|
||||

|
||||
# Usage
|
||||
|
||||
The default variation is the static colored wordmark in horizontal layout and dark text on a light background.
|
||||
|
||||
## Layout variations
|
||||
|
||||

|
||||
|
||||
The default layout is the wordmark in horizontal layout. It provides the clearest context to the brand identity of Home Assistant.
|
||||
|
||||
Use the logomark variant when the context is clear that the logo is about Home Assistant. For example, inside the Home Assistant app where users are already aware of where they are at, the logomark variant without the wordmark can be used. The logomark can exist without the wordmark, however, the wordmark should never exist without the icon.
|
||||
|
||||
Use the wordmark in vertical layout when the space available has an aspect ratio less than 4:3. For example, in a square space on a t-shirt where a logo is needed, since there is no established context of Home Assistant, the wordmark in vertical layout should be used.
|
||||
|
||||
Lastly, use the wordmark in vertical layout with small logomark when Home Assistant is displayed in context of other Home Assistant-related projects. For example, in a flowchart showing the voice pipeline, use this layout for Home Assistant and its other related projects.
|
||||
|
||||
## Color variations, backgrounds, and placement
|
||||
|
||||
The default color is the colored version on light background with dark text.
|
||||
|
||||
For backgrounds that are dark, for example, when it is used on a page in a dark theme, use the colored version on dark background with light text.
|
||||
|
||||
In printed materials where color is unavailable, use the monochrome color variations.
|
||||
|
||||
On background that are dark or photographic, use the light monochrome color on dark background variation.
|
||||
|
||||
On backgrounds that are light or photographic, use the colored version. Do not use the monochrome variations.
|
||||
|
||||
Do not enclose the logmark in a square or color or any confined backgrounds, except in specific situations enforced by another company's marketplace guidelines, for example, an iOS app icon.
|
||||
|
||||
Do not add drop shadow to the logomark or the wordmark. If legibility is compromised due to the background, change the background to provide more contrast, or in last resort, add a heavily blurred drop shadaow.
|
||||
|
||||
It should only be used with black, white, and non-duotone photography.
|
||||
|
||||
Unlike the previous version of our logo, no outlined variants are available. Use the monochrome variants in those spaces.
|
||||
|
||||
### Exclusion zone
|
||||
|
||||
The logo needs some personal space. Its exclusion zone is equal to a quarter the height of the icon.
|
||||
|
||||

|
||||
|
||||
## Animation
|
||||
|
||||
The default is the static variant.
|
||||
|
||||
Use the animated variant only for introductory purposes, for example, in the beginning of a video or on a loading screen.
|
||||
|
||||
Use the animated with sound variant only when sound is warranted in the user's context. For example, use it in the beginning of a video since sounds are expected in a video, but do not use it on a loading screen since sounds are not expected in a user interface.
|
||||
|
||||
Do not repeat the logo animation.
|
||||
|
||||
## Sizes and app icon variants
|
||||
|
||||
Special variants are created for specific contexts.
|
||||
|
||||
Use the tiny variants when the logomark is used in a very small space (16x16 dp), for example, the favicon of the Home Assistant website, a notification on Android, or the menubar of macOS.
|
||||
|
@@ -343,7 +343,7 @@ export class DemoEntityState extends LitElement {
|
||||
const columns: DataTableColumnContainer<EntityRowData> = {
|
||||
icon: {
|
||||
title: "Icon",
|
||||
template: (_, entry) => html`
|
||||
template: (entry) => html`
|
||||
<state-badge
|
||||
.stateObj=${entry.stateObj}
|
||||
.stateColor=${true}
|
||||
@@ -360,7 +360,7 @@ export class DemoEntityState extends LitElement {
|
||||
title: "State",
|
||||
width: "20%",
|
||||
sortable: true,
|
||||
template: (_, entry) =>
|
||||
template: (entry) =>
|
||||
html`${computeStateDisplay(
|
||||
hass.localize,
|
||||
entry.stateObj,
|
||||
@@ -371,14 +371,14 @@ export class DemoEntityState extends LitElement {
|
||||
},
|
||||
device_class: {
|
||||
title: "Device class",
|
||||
template: (dc) => html`${dc ?? "-"}`,
|
||||
template: (entry) => html`${entry.device_class ?? "-"}`,
|
||||
width: "20%",
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
},
|
||||
domain: {
|
||||
title: "Domain",
|
||||
template: (_, entry) => html`${computeDomain(entry.entity_id)}`,
|
||||
template: (entry) => html`${computeDomain(entry.entity_id)}`,
|
||||
width: "20%",
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
|
@@ -49,6 +49,10 @@ import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hass
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
type BackupItem = HassioBackup & {
|
||||
secondary: string;
|
||||
};
|
||||
|
||||
@customElement("hassio-backups")
|
||||
export class HassioBackups extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -117,15 +121,15 @@ export class HassioBackups extends LitElement {
|
||||
}
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer => ({
|
||||
(narrow: boolean): DataTableColumnContainer<BackupItem> => ({
|
||||
name: {
|
||||
title: this.supervisor.localize("backup.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
template: (entry: string, backup: any) =>
|
||||
html`${entry || backup.slug}
|
||||
template: (backup) =>
|
||||
html`${backup.name || backup.slug}
|
||||
<div class="secondary">${backup.secondary}</div>`,
|
||||
},
|
||||
size: {
|
||||
@@ -134,7 +138,7 @@ export class HassioBackups extends LitElement {
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (entry: number) => Math.ceil(entry * 10) / 10 + " MB",
|
||||
template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
|
||||
},
|
||||
location: {
|
||||
title: this.supervisor.localize("backup.location"),
|
||||
@@ -142,8 +146,8 @@ export class HassioBackups extends LitElement {
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (entry: string | null) =>
|
||||
entry || this.supervisor.localize("backup.data_disk"),
|
||||
template: (backup) =>
|
||||
backup.location || this.supervisor.localize("backup.data_disk"),
|
||||
},
|
||||
date: {
|
||||
title: this.supervisor.localize("backup.created"),
|
||||
@@ -152,8 +156,8 @@ export class HassioBackups extends LitElement {
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (entry: string) =>
|
||||
relativeTime(new Date(entry), this.hass.locale),
|
||||
template: (backup) =>
|
||||
relativeTime(new Date(backup.date), this.hass.locale),
|
||||
},
|
||||
secondary: {
|
||||
title: "",
|
||||
@@ -163,7 +167,7 @@ export class HassioBackups extends LitElement {
|
||||
})
|
||||
);
|
||||
|
||||
private _backupData = memoizeOne((backups: HassioBackup[]) =>
|
||||
private _backupData = memoizeOne((backups: HassioBackup[]): BackupItem[] =>
|
||||
backups.map((backup) => ({
|
||||
...backup,
|
||||
secondary: this._computeBackupContent(backup),
|
||||
|
@@ -33,7 +33,6 @@ export class HassioUploadBackup extends LitElement {
|
||||
label="Upload backup"
|
||||
supports="Supports .TAR files"
|
||||
@file-picked=${this._uploadFile}
|
||||
auto-open-file-dialog
|
||||
></ha-file-upload>
|
||||
`;
|
||||
}
|
||||
|
73
package.json
73
package.json
@@ -25,24 +25,24 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.22.11",
|
||||
"@babel/runtime": "7.22.15",
|
||||
"@braintree/sanitize-url": "6.0.4",
|
||||
"@codemirror/autocomplete": "6.9.0",
|
||||
"@codemirror/autocomplete": "6.9.1",
|
||||
"@codemirror/commands": "6.2.5",
|
||||
"@codemirror/language": "6.9.0",
|
||||
"@codemirror/legacy-modes": "6.3.3",
|
||||
"@codemirror/search": "6.5.2",
|
||||
"@codemirror/search": "6.5.3",
|
||||
"@codemirror/state": "6.2.1",
|
||||
"@codemirror/view": "6.17.1",
|
||||
"@codemirror/view": "6.19.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.10.0",
|
||||
"@formatjs/intl-displaynames": "6.5.0",
|
||||
"@formatjs/intl-datetimeformat": "6.10.2",
|
||||
"@formatjs/intl-displaynames": "6.5.2",
|
||||
"@formatjs/intl-getcanonicallocales": "2.2.1",
|
||||
"@formatjs/intl-listformat": "7.4.0",
|
||||
"@formatjs/intl-locale": "3.3.2",
|
||||
"@formatjs/intl-numberformat": "8.7.0",
|
||||
"@formatjs/intl-pluralrules": "5.2.4",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.4",
|
||||
"@formatjs/intl-listformat": "7.4.2",
|
||||
"@formatjs/intl-locale": "3.3.4",
|
||||
"@formatjs/intl-numberformat": "8.7.2",
|
||||
"@formatjs/intl-pluralrules": "5.2.6",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.6",
|
||||
"@fullcalendar/core": "6.1.8",
|
||||
"@fullcalendar/daygrid": "6.1.8",
|
||||
"@fullcalendar/interaction": "6.1.8",
|
||||
@@ -50,10 +50,10 @@
|
||||
"@fullcalendar/luxon3": "6.1.8",
|
||||
"@fullcalendar/timegrid": "6.1.8",
|
||||
"@lezer/highlight": "1.1.6",
|
||||
"@lit-labs/context": "0.4.0",
|
||||
"@lit-labs/context": "0.4.1",
|
||||
"@lit-labs/motion": "1.0.4",
|
||||
"@lit-labs/virtualizer": "2.0.7",
|
||||
"@lrnwebcomponents/simple-tooltip": "7.0.16",
|
||||
"@lrnwebcomponents/simple-tooltip": "7.0.18",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/mwc-button": "0.27.0",
|
||||
@@ -79,7 +79,7 @@
|
||||
"@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": "=1.0.0-pre.16",
|
||||
"@material/web": "=1.0.0-pre.17",
|
||||
"@mdi/js": "7.2.96",
|
||||
"@mdi/svg": "7.2.96",
|
||||
"@polymer/iron-flex-layout": "3.0.1",
|
||||
@@ -93,8 +93,8 @@
|
||||
"@polymer/paper-toast": "3.0.1",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.1.6",
|
||||
"@vaadin/vaadin-themable-mixin": "24.1.6",
|
||||
"@vaadin/combo-box": "24.1.7",
|
||||
"@vaadin/vaadin-themable-mixin": "24.1.7",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@@ -104,7 +104,7 @@
|
||||
"app-datepicker": "5.1.1",
|
||||
"chart.js": "4.3.3",
|
||||
"comlink": "4.4.1",
|
||||
"core-js": "3.32.1",
|
||||
"core-js": "3.32.2",
|
||||
"cropperjs": "1.6.0",
|
||||
"date-fns": "2.30.0",
|
||||
"date-fns-tz": "2.0.0",
|
||||
@@ -115,12 +115,12 @@
|
||||
"hls.js": "1.4.12",
|
||||
"home-assistant-js-websocket": "8.2.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.5.0",
|
||||
"intl-messageformat": "10.5.2",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "1.0.4",
|
||||
"lit": "2.8.0",
|
||||
"luxon": "3.4.2",
|
||||
"luxon": "3.4.3",
|
||||
"marked": "7.0.5",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
@@ -137,9 +137,9 @@
|
||||
"tinykeys": "2.1.0",
|
||||
"tsparticles-engine": "2.12.0",
|
||||
"tsparticles-preset-links": "2.12.0",
|
||||
"ua-parser-js": "1.0.35",
|
||||
"ua-parser-js": "1.0.36",
|
||||
"unfetch": "5.0.0",
|
||||
"vis-data": "7.1.6",
|
||||
"vis-data": "7.1.7",
|
||||
"vis-network": "9.1.6",
|
||||
"vue": "2.7.14",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
@@ -153,12 +153,13 @@
|
||||
"xss": "1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.22.11",
|
||||
"@babel/plugin-proposal-decorators": "7.22.10",
|
||||
"@babel/plugin-transform-runtime": "7.22.10",
|
||||
"@babel/preset-env": "7.22.14",
|
||||
"@babel/preset-typescript": "7.22.11",
|
||||
"@babel/core": "7.22.20",
|
||||
"@babel/plugin-proposal-decorators": "7.22.15",
|
||||
"@babel/plugin-transform-runtime": "7.22.15",
|
||||
"@babel/preset-env": "7.22.20",
|
||||
"@babel/preset-typescript": "7.22.15",
|
||||
"@koa/cors": "4.0.0",
|
||||
"@lokalise/node-api": "11.0.1",
|
||||
"@octokit/auth-oauth-device": "6.0.0",
|
||||
"@octokit/plugin-retry": "6.0.0",
|
||||
"@octokit/rest": "20.0.1",
|
||||
@@ -168,13 +169,13 @@
|
||||
"@rollup/plugin-json": "6.0.0",
|
||||
"@rollup/plugin-node-resolve": "15.2.1",
|
||||
"@rollup/plugin-replace": "5.0.2",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.2",
|
||||
"@types/chromecast-caf-receiver": "6.0.9",
|
||||
"@types/chromecast-caf-sender": "1.0.5",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.3",
|
||||
"@types/chromecast-caf-receiver": "6.0.10",
|
||||
"@types/chromecast-caf-sender": "1.0.6",
|
||||
"@types/esprima": "4.0.3",
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/html-minifier-terser": "7.0.0",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
"@types/js-yaml": "4.0.6",
|
||||
"@types/leaflet": "1.9.4",
|
||||
"@types/leaflet-draw": "1.0.8",
|
||||
"@types/luxon": "3.3.2",
|
||||
@@ -182,18 +183,18 @@
|
||||
"@types/qrcode": "1.5.2",
|
||||
"@types/serve-handler": "6.1.1",
|
||||
"@types/sortablejs": "1.15.2",
|
||||
"@types/tar": "6.1.5",
|
||||
"@types/tar": "6.1.6",
|
||||
"@types/ua-parser-js": "0.7.37",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "6.5.0",
|
||||
"@typescript-eslint/parser": "6.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.0",
|
||||
"@typescript-eslint/parser": "6.7.0",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"chai": "4.3.8",
|
||||
"del": "7.1.0",
|
||||
"eslint": "8.48.0",
|
||||
"eslint": "8.49.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-airbnb-typescript": "17.1.0",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
@@ -233,10 +234,10 @@
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"rollup-plugin-visualizer": "5.9.2",
|
||||
"serve-handler": "6.1.5",
|
||||
"sinon": "15.2.0",
|
||||
"sinon": "16.0.0",
|
||||
"source-map-url": "0.4.1",
|
||||
"systemjs": "6.14.2",
|
||||
"tar": "6.1.15",
|
||||
"tar": "6.2.0",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"ts-lit-plugin": "2.0.0-pre.1",
|
||||
"typescript": "5.2.2",
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20230904.0"
|
||||
version = "20230911.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -8,40 +8,4 @@ set -eu -o pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ -z "${LOKALISE_TOKEN-}" ] && [ ! -f .lokalise_token ] ; then
|
||||
echo "Lokalise API token is required to download the latest set of" \
|
||||
"translations. Please create an account by using the following link:" \
|
||||
"https://lokalise.co/signup/3420425759f6d6d241f598.13594006/all/" \
|
||||
"Place your token in a new file \".lokalise_token\" in the repo" \
|
||||
"root directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Load token from file if not already in the environment
|
||||
[ -z "${LOKALISE_TOKEN-}" ] && LOKALISE_TOKEN="$(<.lokalise_token)"
|
||||
|
||||
declare -A PROJECT_ID=( \
|
||||
[frontend]="3420425759f6d6d241f598.13594006" \
|
||||
[backend]="130246255a974bd3b5e8a1.51616605" \
|
||||
)
|
||||
|
||||
for project in ${!PROJECT_ID[*]}; do
|
||||
LOCAL_DIR=`pwd`/translations/${project}
|
||||
rm -f ${LOCAL_DIR}/* || mkdir -p ${LOCAL_DIR}
|
||||
docker run \
|
||||
-v ${LOCAL_DIR}:/opt/dest/locale \
|
||||
--rm \
|
||||
lokalise/lokalise-cli-2@sha256:f1860b26be22fa73b8c93bc5f8690f2afc867610a42de6fc27adc790e5d4425d \
|
||||
lokalise2 \
|
||||
--token ${LOKALISE_TOKEN} \
|
||||
--project-id ${PROJECT_ID[${project}]} \
|
||||
file download \
|
||||
--export-empty-as skip \
|
||||
--format json \
|
||||
--json-unescaped-slashes=true \
|
||||
--replace-breaks=false \
|
||||
--original-filenames=false \
|
||||
--unzip-to /opt/dest
|
||||
done
|
||||
|
||||
./node_modules/.bin/gulp check-downloaded-translations
|
||||
./node_modules/.bin/gulp download-translations
|
@@ -35,20 +35,47 @@ export class HaPasswordManagerPolyfill extends LitElement {
|
||||
super.connectedCallback();
|
||||
this._styleElement = document.createElement("style");
|
||||
this._styleElement.textContent = css`
|
||||
/* Polyfill form is sized and vertically aligned with true form, then positioned offscreen
|
||||
rather than hiding so it does not create a new stacking context */
|
||||
.password-manager-polyfill {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
z-index: -1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.password-manager-polyfill input {
|
||||
/* Excluding our wrapper, move any children back on screen, including anything injected that might not already be positioned */
|
||||
.password-manager-polyfill > *:not(.wrapper),
|
||||
.password-manager-polyfill > .wrapper > * {
|
||||
position: relative;
|
||||
left: 10000px;
|
||||
}
|
||||
/* Size and hide our polyfill fields */
|
||||
.password-manager-polyfill .underneath {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 62px;
|
||||
padding: 0;
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
z-index: -1;
|
||||
height: 21px;
|
||||
/* Transparency is only needed to hide during paint or in case of misalignment,
|
||||
but LastPass will fail if it's 0, so we use 1% */
|
||||
opacity: 0.01;
|
||||
}
|
||||
.password-manager-polyfill input[type="submit"] {
|
||||
width: 0;
|
||||
height: 0;
|
||||
.password-manager-polyfill input.underneath {
|
||||
height: 28px;
|
||||
margin-bottom: 30.5px;
|
||||
}
|
||||
/* Button position is not important, but size should not be zero */
|
||||
.password-manager-polyfill > input.underneath[type="submit"] {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Ensure injected elements will be on top */
|
||||
.password-manager-polyfill > *:not(.underneath, .wrapper),
|
||||
.password-manager-polyfill > .wrapper > *:not(.underneath) {
|
||||
isolation: isolate;
|
||||
z-index: auto;
|
||||
}
|
||||
`.toString();
|
||||
document.head.append(this._styleElement);
|
||||
@@ -77,16 +104,25 @@ export class HaPasswordManagerPolyfill extends LitElement {
|
||||
class="password-manager-polyfill"
|
||||
style=${styleMap({
|
||||
top: `${this.boundingRect?.y || 148}px`,
|
||||
left: `calc(50% - ${(this.boundingRect?.width || 360) / 2}px)`,
|
||||
left: `calc(50% - ${
|
||||
(this.boundingRect?.width || 360) / 2
|
||||
}px - 10000px)`,
|
||||
width: `${this.boundingRect?.width || 360}px`,
|
||||
})}
|
||||
aria-hidden="true"
|
||||
action="/auth"
|
||||
method="post"
|
||||
@submit=${this._handleSubmit}
|
||||
>
|
||||
${autocompleteLoginFields(this.step.data_schema).map((input) =>
|
||||
this.render_input(input)
|
||||
)}
|
||||
<input type="submit" />
|
||||
<input
|
||||
type="submit"
|
||||
value="Login"
|
||||
class="underneath"
|
||||
tabindex="-2"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
@@ -99,26 +135,35 @@ export class HaPasswordManagerPolyfill extends LitElement {
|
||||
return "";
|
||||
}
|
||||
return html`
|
||||
<input
|
||||
tabindex="-1"
|
||||
.id=${schema.name}
|
||||
.name=${schema.name}
|
||||
.type=${inputType}
|
||||
.value=${this.stepData[schema.name] || ""}
|
||||
.autocomplete=${schema.autocomplete}
|
||||
@input=${this._valueChanged}
|
||||
@change=${this._valueChanged}
|
||||
/>
|
||||
<!-- Label is a sibling so it can be stacked underneath without affecting injections adjacent to input (e.g. LastPass) -->
|
||||
<label for=${schema.name} class="underneath" aria-hidden="true">
|
||||
${schema.name}
|
||||
</label>
|
||||
<!-- LastPass fails if the input is hidden directly, so we trick it and hide a wrapper instead -->
|
||||
<div class="wrapper" aria-hidden="true">
|
||||
<!-- LastPass fails with tabindex of -1, so we trick with -2 -->
|
||||
<input
|
||||
class="underneath"
|
||||
tabindex="-2"
|
||||
.id=${schema.name}
|
||||
.name=${schema.name}
|
||||
.type=${inputType}
|
||||
.value=${this.stepData[schema.name] || ""}
|
||||
.autocomplete=${schema.autocomplete}
|
||||
@input=${this._valueChanged}
|
||||
@change=${this._valueChanged}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleSubmit(ev: Event) {
|
||||
private _handleSubmit(ev: SubmitEvent) {
|
||||
ev.preventDefault();
|
||||
fireEvent(this, "form-submitted");
|
||||
}
|
||||
|
||||
private _valueChanged(ev: Event) {
|
||||
const target = ev.target! as HTMLInputElement;
|
||||
const target = ev.target as HTMLInputElement;
|
||||
this.stepData = { ...this.stepData, [target.id]: target.value };
|
||||
fireEvent(this, "value-changed", {
|
||||
value: this.stepData,
|
||||
|
@@ -22,14 +22,7 @@ export type LocalizeKeys =
|
||||
| `ui.dialogs.unhealthy.reason.${string}`
|
||||
| `ui.dialogs.unsupported.reason.${string}`
|
||||
| `ui.panel.config.${string}.${"caption" | "description"}`
|
||||
| `ui.panel.config.automation.${string}`
|
||||
| `ui.panel.config.dashboard.${string}`
|
||||
| `ui.panel.config.devices.${string}`
|
||||
| `ui.panel.config.energy.${string}`
|
||||
| `ui.panel.config.info.${string}`
|
||||
| `ui.panel.config.lovelace.${string}`
|
||||
| `ui.panel.config.network.${string}`
|
||||
| `ui.panel.config.scene.${string}`
|
||||
| `ui.panel.config.zha.${string}`
|
||||
| `ui.panel.config.zwave_js.${string}`
|
||||
| `ui.panel.lovelace.card.${string}`
|
||||
|
@@ -349,9 +349,6 @@ export class HaChartBase extends LitElement {
|
||||
height: 0;
|
||||
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.chartContainer {
|
||||
position: relative;
|
||||
}
|
||||
canvas {
|
||||
max-height: var(--chart-max-height, 400px);
|
||||
}
|
||||
|
@@ -74,7 +74,7 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
||||
title: TemplateResult | string;
|
||||
label?: TemplateResult | string;
|
||||
type?: "numeric" | "icon" | "icon-button" | "overflow-menu" | "flex";
|
||||
template?: (data: any, row: T) => TemplateResult | string | typeof nothing;
|
||||
template?: (row: T) => TemplateResult | string | typeof nothing;
|
||||
width?: string;
|
||||
maxWidth?: string;
|
||||
grows?: boolean;
|
||||
@@ -431,7 +431,7 @@ export class HaDataTable extends LitElement {
|
||||
})
|
||||
: ""}
|
||||
>
|
||||
${column.template ? column.template(row[key], row) : row[key]}
|
||||
${column.template ? column.template(row) : row[key]}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import { Ripple } from "@material/mwc-ripple";
|
||||
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
|
||||
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
||||
import { mdiMenuDown } from "@mdi/js";
|
||||
import { css, html, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
queryAsync,
|
||||
state,
|
||||
@@ -24,6 +26,12 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
|
||||
@query(".select-anchor") protected anchorElement!: HTMLDivElement | null;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-arrow" })
|
||||
public showArrow?: boolean;
|
||||
|
||||
@property({ type: Boolean, attribute: "hide-label" })
|
||||
public hideLabel?: boolean;
|
||||
|
||||
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
|
||||
|
||||
@state() private _shouldRenderRipple = false;
|
||||
@@ -36,7 +44,9 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
"select-no-value": !this.selectedText,
|
||||
};
|
||||
|
||||
const labelledby = this.label ? "label" : undefined;
|
||||
const labelledby = this.label && !this.hideLabel ? "label" : undefined;
|
||||
const labelAttribute =
|
||||
this.label && this.hideLabel ? this.label : undefined;
|
||||
|
||||
return html`
|
||||
<div class="select ${classMap(classes)}">
|
||||
@@ -57,6 +67,7 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
aria-invalid=${!this.isUiValid}
|
||||
aria-haspopup="listbox"
|
||||
aria-labelledby=${ifDefined(labelledby)}
|
||||
aria-label=${ifDefined(labelAttribute)}
|
||||
aria-required=${this.required}
|
||||
@click=${this.onClick}
|
||||
@focus=${this.onFocus}
|
||||
@@ -72,11 +83,14 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
>
|
||||
${this.renderIcon()}
|
||||
<div class="content">
|
||||
<p id="label" class="label">${this.label}</p>
|
||||
${this.hideLabel
|
||||
? nothing
|
||||
: html`<p id="label" class="label">${this.label}</p>`}
|
||||
${this.selectedText
|
||||
? html`<p class="value">${this.selectedText}</p>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this.renderArrow()}
|
||||
${this._shouldRenderRipple && !this.disabled
|
||||
? html` <mwc-ripple></mwc-ripple> `
|
||||
: nothing}
|
||||
@@ -86,13 +100,29 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderArrow() {
|
||||
if (!this.showArrow) return nothing;
|
||||
|
||||
return html`
|
||||
<div class="icon">
|
||||
<ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderIcon() {
|
||||
const index = this.mdcFoundation?.getSelectedIndex();
|
||||
const items = this.menuElement?.items ?? [];
|
||||
const item = index != null ? items[index] : undefined;
|
||||
const icon =
|
||||
item?.querySelector("[slot='graphic']") ??
|
||||
(null as HaSvgIcon | HaIcon | null);
|
||||
const defaultIcon = this.querySelector("[slot='icon']");
|
||||
const icon = (item?.querySelector("[slot='graphic']") ?? null) as
|
||||
| HaSvgIcon
|
||||
| HaIcon
|
||||
| null;
|
||||
|
||||
if (!defaultIcon && !icon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="icon">
|
||||
@@ -171,14 +201,18 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
--control-select-menu-background-color: var(--disabled-color);
|
||||
--control-select-menu-background-opacity: 0.2;
|
||||
--control-select-menu-border-radius: 14px;
|
||||
--control-select-menu-height: 48px;
|
||||
--control-select-menu-padding: 6px 10px;
|
||||
--mdc-icon-size: 20px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
width: auto;
|
||||
color: var(--primary-text-color);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.select-anchor {
|
||||
height: 48px;
|
||||
padding: 6px 10px;
|
||||
height: var(--control-select-menu-height);
|
||||
padding: var(--control-select-menu-padding);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
@@ -193,15 +227,12 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
--mdc-ripple-color: var(--control-select-menu-background-color);
|
||||
/* For safari border-radius overflow */
|
||||
z-index: 0;
|
||||
font-size: inherit;
|
||||
transition: color 180ms ease-in-out;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.25px;
|
||||
}
|
||||
.content {
|
||||
@@ -223,8 +254,7 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-size: 0.85em;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
|
@@ -155,11 +155,12 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
if (!this._configEntry) {
|
||||
return;
|
||||
}
|
||||
showOptionsFlowDialog(
|
||||
this,
|
||||
this._configEntry,
|
||||
await fetchIntegrationManifest(this.hass, this._configEntry.domain)
|
||||
);
|
||||
showOptionsFlowDialog(this, this._configEntry, {
|
||||
manifest: await fetchIntegrationManifest(
|
||||
this.hass,
|
||||
this._configEntry.domain
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -27,7 +27,8 @@ export const computeInitialHaFormData = (
|
||||
data[field.name] = 0.0;
|
||||
} else if (field.type === "select") {
|
||||
if (field.options.length) {
|
||||
data[field.name] = field.options[0][0];
|
||||
const val = field.options[0];
|
||||
data[field.name] = Array.isArray(val) ? val[0] : val;
|
||||
}
|
||||
} else if (field.type === "positive_time_period_dict") {
|
||||
data[field.name] = {
|
||||
@@ -60,8 +61,10 @@ export const computeInitialHaFormData = (
|
||||
data[field.name] = selector.number?.min ?? 0;
|
||||
} else if ("select" in selector) {
|
||||
if (selector.select?.options.length) {
|
||||
const val = selector.select.options[0];
|
||||
data[field.name] = Array.isArray(val) ? val[0] : val;
|
||||
const firstOption = selector.select.options[0];
|
||||
const val =
|
||||
typeof firstOption === "string" ? firstOption : firstOption.value;
|
||||
data[field.name] = selector.select.multiple ? [val] : val;
|
||||
}
|
||||
} else if ("duration" in selector) {
|
||||
data[field.name] = {
|
||||
|
@@ -238,11 +238,13 @@ export interface ZoneCondition extends BaseCondition {
|
||||
zone: string;
|
||||
}
|
||||
|
||||
type Weekday = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat";
|
||||
|
||||
export interface TimeCondition extends BaseCondition {
|
||||
condition: "time";
|
||||
after?: string;
|
||||
before?: string;
|
||||
weekday?: string | string[];
|
||||
weekday?: Weekday | Weekday[];
|
||||
}
|
||||
|
||||
export interface TemplateCondition extends BaseCondition {
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import { EntityFilter } from "../common/entity/entity_filter";
|
||||
import { PlaceholderContainer } from "../panels/config/automation/thingtalk/dialog-thingtalk";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { AutomationConfig } from "./automation";
|
||||
|
||||
interface CloudStatusNotLoggedIn {
|
||||
logged_in: false;
|
||||
@@ -13,6 +11,7 @@ export interface CertificateInformation {
|
||||
common_name: string;
|
||||
expire_date: string;
|
||||
fingerprint: string;
|
||||
alternative_names: string[];
|
||||
}
|
||||
|
||||
export interface CloudPreferences {
|
||||
@@ -66,11 +65,6 @@ export interface CloudWebhook {
|
||||
managed?: boolean;
|
||||
}
|
||||
|
||||
export interface ThingTalkConversion {
|
||||
config: Partial<AutomationConfig>;
|
||||
placeholders: PlaceholderContainer;
|
||||
}
|
||||
|
||||
export const cloudLogin = (
|
||||
hass: HomeAssistant,
|
||||
email: string,
|
||||
@@ -136,9 +130,6 @@ export const disconnectCloudRemote = (hass: HomeAssistant) =>
|
||||
export const fetchCloudSubscriptionInfo = (hass: HomeAssistant) =>
|
||||
hass.callWS<SubscriptionInfo>({ type: "cloud/subscription" });
|
||||
|
||||
export const convertThingTalk = (hass: HomeAssistant, query: string) =>
|
||||
hass.callWS<ThingTalkConversion>({ type: "cloud/thingtalk/convert", query });
|
||||
|
||||
export const updateCloudPref = (
|
||||
hass: HomeAssistant,
|
||||
prefs: {
|
||||
|
@@ -1,46 +1,25 @@
|
||||
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
interface EntitySourceConfigEntry {
|
||||
source: "config_entry";
|
||||
interface EntitySource {
|
||||
domain: string;
|
||||
custom_component: boolean;
|
||||
config_entry: string;
|
||||
}
|
||||
|
||||
interface EntitySourcePlatformConfig {
|
||||
source: "platform_config";
|
||||
domain: string;
|
||||
custom_component: boolean;
|
||||
}
|
||||
export type EntitySources = Record<string, EntitySource>;
|
||||
|
||||
export type EntitySources = Record<
|
||||
string,
|
||||
EntitySourceConfigEntry | EntitySourcePlatformConfig
|
||||
>;
|
||||
|
||||
const fetchEntitySources = (
|
||||
hass: HomeAssistant,
|
||||
entity_id?: string
|
||||
): Promise<EntitySources> =>
|
||||
hass.callWS({
|
||||
type: "entity/source",
|
||||
entity_id,
|
||||
});
|
||||
const fetchEntitySources = (hass: HomeAssistant): Promise<EntitySources> =>
|
||||
hass.callWS({ type: "entity/source" });
|
||||
|
||||
export const fetchEntitySourcesWithCache = (
|
||||
hass: HomeAssistant,
|
||||
entity_id?: string
|
||||
hass: HomeAssistant
|
||||
): Promise<EntitySources> =>
|
||||
entity_id
|
||||
? fetchEntitySources(hass, entity_id)
|
||||
: timeCachePromiseFunc(
|
||||
"_entitySources",
|
||||
// cache for 30 seconds
|
||||
30000,
|
||||
fetchEntitySources,
|
||||
// We base the cache on number of states. If number of states
|
||||
// changes we force a refresh
|
||||
(hass2) => Object.keys(hass2.states).length,
|
||||
hass
|
||||
);
|
||||
timeCachePromiseFunc(
|
||||
"_entitySources",
|
||||
// cache for 30 seconds
|
||||
30000,
|
||||
fetchEntitySources,
|
||||
// We base the cache on number of states. If number of states
|
||||
// changes we force a refresh
|
||||
(hass2) => Object.keys(hass2.states).length,
|
||||
hass
|
||||
);
|
||||
|
@@ -36,7 +36,9 @@ export const enum WeatherEntityFeature {
|
||||
FORECAST_TWICE_DAILY = 4,
|
||||
}
|
||||
|
||||
export type ForecastType = "legacy" | "hourly" | "daily" | "twice_daily";
|
||||
export type ModernForecastType = "hourly" | "daily" | "twice_daily";
|
||||
|
||||
export type ForecastType = ModernForecastType | "legacy";
|
||||
|
||||
interface ForecastAttribute {
|
||||
temperature: number;
|
||||
@@ -636,7 +638,7 @@ export const getForecast = (
|
||||
export const subscribeForecast = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
forecast_type: "daily" | "hourly" | "twice_daily",
|
||||
forecast_type: ModernForecastType,
|
||||
callback: (forecastevent: ForecastEvent) => void
|
||||
) =>
|
||||
hass.connection.subscribeMessage<ForecastEvent>(callback, {
|
||||
@@ -645,6 +647,22 @@ export const subscribeForecast = (
|
||||
entity_id,
|
||||
});
|
||||
|
||||
export const getSupportedForecastTypes = (
|
||||
stateObj: HassEntityBase
|
||||
): ModernForecastType[] => {
|
||||
const supported: ModernForecastType[] = [];
|
||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) {
|
||||
supported.push("daily");
|
||||
}
|
||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_TWICE_DAILY)) {
|
||||
supported.push("twice_daily");
|
||||
}
|
||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_HOURLY)) {
|
||||
supported.push("hourly");
|
||||
}
|
||||
return supported;
|
||||
};
|
||||
|
||||
export const getDefaultForecastType = (stateObj: HassEntityBase) => {
|
||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) {
|
||||
return "daily";
|
||||
|
@@ -6,7 +6,12 @@ export interface RenderTemplateResult {
|
||||
listeners: TemplateListeners;
|
||||
}
|
||||
|
||||
interface TemplateListeners {
|
||||
export interface RenderTemplateError {
|
||||
error: string;
|
||||
level: "ERROR" | "WARNING";
|
||||
}
|
||||
|
||||
export interface TemplateListeners {
|
||||
all: boolean;
|
||||
domains: string[];
|
||||
entities: string[];
|
||||
@@ -18,6 +23,7 @@ export type TemplatePreview = TemplatePreviewState | TemplatePreviewError;
|
||||
interface TemplatePreviewState {
|
||||
state: string;
|
||||
attributes: Record<string, any>;
|
||||
listeners: TemplateListeners;
|
||||
}
|
||||
|
||||
interface TemplatePreviewError {
|
||||
@@ -26,19 +32,23 @@ interface TemplatePreviewError {
|
||||
|
||||
export const subscribeRenderTemplate = (
|
||||
conn: Connection,
|
||||
onChange: (result: RenderTemplateResult) => void,
|
||||
onChange: (result: RenderTemplateResult | RenderTemplateError) => void,
|
||||
params: {
|
||||
template: string;
|
||||
entity_ids?: string | string[];
|
||||
variables?: Record<string, unknown>;
|
||||
timeout?: number;
|
||||
strict?: boolean;
|
||||
report_errors?: boolean;
|
||||
}
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
conn.subscribeMessage((msg: RenderTemplateResult) => onChange(msg), {
|
||||
type: "render_template",
|
||||
...params,
|
||||
});
|
||||
conn.subscribeMessage(
|
||||
(msg: RenderTemplateResult | RenderTemplateError) => onChange(msg),
|
||||
{
|
||||
type: "render_template",
|
||||
...params,
|
||||
}
|
||||
);
|
||||
|
||||
export const subscribePreviewTemplate = (
|
||||
hass: HomeAssistant,
|
||||
|
@@ -49,7 +49,7 @@ class FlowPreviewGroup extends LitElement {
|
||||
private _setPreview = (preview: GroupPreview) => {
|
||||
const now = new Date().toISOString();
|
||||
this._preview = {
|
||||
entity_id: `${this.stepId}.flow_preview`,
|
||||
entity_id: `${this.stepId}.___flow_preview___`,
|
||||
last_changed: now,
|
||||
last_updated: now,
|
||||
context: { id: "", parent_id: null, user_id: null },
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { LitElement, html } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { FlowType } from "../../../data/data_entry_flow";
|
||||
import {
|
||||
TemplateListeners,
|
||||
TemplatePreview,
|
||||
subscribePreviewTemplate,
|
||||
} from "../../../data/ws-templates";
|
||||
@@ -27,6 +28,8 @@ class FlowPreviewTemplate extends LitElement {
|
||||
|
||||
@state() private _preview?: HassEntity;
|
||||
|
||||
@state() private _listeners?: TemplateListeners;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _unsub?: Promise<UnsubscribeFunc>;
|
||||
@@ -50,9 +53,69 @@ class FlowPreviewTemplate extends LitElement {
|
||||
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
||||
}
|
||||
return html`<entity-preview-row
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this._preview}
|
||||
></entity-preview-row>`;
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this._preview}
|
||||
></entity-preview-row>
|
||||
${this._listeners?.time
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize("ui.dialogs.helper_settings.template.time")}
|
||||
</p>
|
||||
`
|
||||
: nothing}
|
||||
${!this._listeners
|
||||
? nothing
|
||||
: this._listeners.all
|
||||
? html`
|
||||
<p class="all_listeners">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.template.all_listeners"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: this._listeners.domains.length || this._listeners.entities.length
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.template.listeners"
|
||||
)}
|
||||
</p>
|
||||
<ul>
|
||||
${this._listeners.domains
|
||||
.sort()
|
||||
.map(
|
||||
(domain) => html`
|
||||
<li>
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.template.domain"
|
||||
)}</b
|
||||
>: ${domain}
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
${this._listeners.entities
|
||||
.sort()
|
||||
.map(
|
||||
(entity_id) => html`
|
||||
<li>
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.template.entity"
|
||||
)}</b
|
||||
>: ${entity_id}
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>
|
||||
`
|
||||
: !this._listeners.time
|
||||
? html`<p class="all_listeners">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.template.no_listeners"
|
||||
)}
|
||||
</p>`
|
||||
: nothing} `;
|
||||
}
|
||||
|
||||
private _setPreview = (preview: TemplatePreview) => {
|
||||
@@ -62,13 +125,15 @@ class FlowPreviewTemplate extends LitElement {
|
||||
return;
|
||||
}
|
||||
this._error = undefined;
|
||||
this._listeners = preview.listeners;
|
||||
const now = new Date().toISOString();
|
||||
this._preview = {
|
||||
entity_id: `${this.stepId}.flow_preview`,
|
||||
entity_id: `${this.stepId}.___flow_preview___`,
|
||||
last_changed: now,
|
||||
last_updated: now,
|
||||
context: { id: "", parent_id: null, user_id: null },
|
||||
...preview,
|
||||
attributes: preview.attributes,
|
||||
state: preview.state,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { html } from "lit";
|
||||
import { ConfigEntry } from "../../data/config_entries";
|
||||
import { domainToName, IntegrationManifest } from "../../data/integration";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import {
|
||||
createOptionsFlow,
|
||||
deleteOptionsFlow,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
handleOptionsFlowStep,
|
||||
} from "../../data/options_flow";
|
||||
import {
|
||||
DataEntryFlowDialogParams,
|
||||
loadDataEntryFlowDialog,
|
||||
showFlowDialog,
|
||||
} from "./show-dialog-data-entry-flow";
|
||||
@@ -17,14 +18,14 @@ export const loadOptionsFlowDialog = loadDataEntryFlowDialog;
|
||||
export const showOptionsFlowDialog = (
|
||||
element: HTMLElement,
|
||||
configEntry: ConfigEntry,
|
||||
manifest?: IntegrationManifest | null
|
||||
dialogParams?: Omit<DataEntryFlowDialogParams, "flowConfig">
|
||||
): void =>
|
||||
showFlowDialog(
|
||||
element,
|
||||
{
|
||||
startFlowHandler: configEntry.entry_id,
|
||||
domain: configEntry.domain,
|
||||
manifest,
|
||||
...dialogParams,
|
||||
},
|
||||
{
|
||||
flowType: "options_flow",
|
||||
|
@@ -35,9 +35,8 @@ export class HaMoreInfoCoverPosition extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const forcedState = this.stateObj.state === "closed" ? "open" : undefined;
|
||||
|
||||
const color = stateColorCss(this.stateObj, forcedState);
|
||||
const openColor = stateColorCss(this.stateObj, "open");
|
||||
const color = stateColorCss(this.stateObj);
|
||||
|
||||
return html`
|
||||
<ha-control-slider
|
||||
@@ -55,6 +54,8 @@ export class HaMoreInfoCoverPosition extends LitElement {
|
||||
"current_position"
|
||||
)}
|
||||
style=${styleMap({
|
||||
// Use open color for inactive state to avoid grey slider that looks disabled
|
||||
"--state-cover-inactive-color": openColor,
|
||||
"--control-slider-color": color,
|
||||
"--control-slider-background": color,
|
||||
})}
|
||||
|
@@ -72,9 +72,8 @@ export class HaMoreInfoCoverTiltPosition extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const forcedState = this.stateObj.state === "closed" ? "open" : undefined;
|
||||
|
||||
const color = stateColorCss(this.stateObj, forcedState);
|
||||
const openColor = stateColorCss(this.stateObj, "open");
|
||||
const color = stateColorCss(this.stateObj);
|
||||
|
||||
return html`
|
||||
<ha-control-slider
|
||||
@@ -91,6 +90,8 @@ export class HaMoreInfoCoverTiltPosition extends LitElement {
|
||||
"current_tilt_position"
|
||||
)}
|
||||
style=${styleMap({
|
||||
// Use open color for inactive state to avoid grey slider that looks disabled
|
||||
"--state-cover-inactive-color": openColor,
|
||||
"--control-slider-color": color,
|
||||
"--control-slider-background": color,
|
||||
})}
|
||||
|
@@ -1,6 +1,4 @@
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-tab-bar/mwc-tab-bar";
|
||||
import "@material/mwc-tab/mwc-tab";
|
||||
import { mdiEyedropper } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import "@material/mwc-tab";
|
||||
import "@material/mwc-tab-bar";
|
||||
import {
|
||||
mdiEye,
|
||||
mdiGauge,
|
||||
@@ -14,14 +16,17 @@ import {
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { formatDateWeekdayDay } from "../../../common/datetime/format_date";
|
||||
import { formatTimeWeekday } from "../../../common/datetime/format_time";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import {
|
||||
ForecastEvent,
|
||||
ModernForecastType,
|
||||
WeatherEntity,
|
||||
getDefaultForecastType,
|
||||
getForecast,
|
||||
getSupportedForecastTypes,
|
||||
getWind,
|
||||
subscribeForecast,
|
||||
weatherIcons,
|
||||
@@ -36,6 +41,8 @@ class MoreInfoWeather extends LitElement {
|
||||
|
||||
@state() private _forecastEvent?: ForecastEvent;
|
||||
|
||||
@state() private _forecastType?: ModernForecastType;
|
||||
|
||||
@state() private _subscribed?: Promise<() => void>;
|
||||
|
||||
private _unsubscribeForecastEvents() {
|
||||
@@ -43,25 +50,28 @@ class MoreInfoWeather extends LitElement {
|
||||
this._subscribed.then((unsub) => unsub());
|
||||
this._subscribed = undefined;
|
||||
}
|
||||
this._forecastEvent = undefined;
|
||||
}
|
||||
|
||||
private async _subscribeForecastEvents() {
|
||||
this._unsubscribeForecastEvents();
|
||||
if (!this.isConnected || !this.hass || !this.stateObj) {
|
||||
if (
|
||||
!this.isConnected ||
|
||||
!this.hass ||
|
||||
!this.stateObj ||
|
||||
!this._forecastType
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const forecastType = getDefaultForecastType(this.stateObj);
|
||||
if (forecastType) {
|
||||
this._subscribed = subscribeForecast(
|
||||
this.hass!,
|
||||
this.stateObj!.entity_id,
|
||||
forecastType,
|
||||
(event) => {
|
||||
this._forecastEvent = event;
|
||||
}
|
||||
);
|
||||
}
|
||||
this._subscribed = subscribeForecast(
|
||||
this.hass!,
|
||||
this.stateObj!.entity_id,
|
||||
this._forecastType,
|
||||
(event) => {
|
||||
this._forecastEvent = event;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -93,10 +103,10 @@ class MoreInfoWeather extends LitElement {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (changedProps.has("stateObj") || !this._subscribed) {
|
||||
if ((changedProps.has("stateObj") || !this._subscribed) && this.stateObj) {
|
||||
const oldState = changedProps.get("stateObj") as
|
||||
| WeatherEntity
|
||||
| undefined;
|
||||
@@ -104,16 +114,25 @@ class MoreInfoWeather extends LitElement {
|
||||
oldState?.entity_id !== this.stateObj?.entity_id ||
|
||||
!this._subscribed
|
||||
) {
|
||||
this._forecastType = getDefaultForecastType(this.stateObj);
|
||||
this._subscribeForecastEvents();
|
||||
}
|
||||
} else if (changedProps.has("_forecastType")) {
|
||||
this._subscribeForecastEvents();
|
||||
}
|
||||
}
|
||||
|
||||
private _supportedForecasts = memoizeOne((stateObj: WeatherEntity) =>
|
||||
getSupportedForecastTypes(stateObj)
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const supportedForecasts = this._supportedForecasts(this.stateObj);
|
||||
|
||||
const forecastData = getForecast(
|
||||
this.stateObj.attributes,
|
||||
this._forecastEvent
|
||||
@@ -210,6 +229,23 @@ class MoreInfoWeather extends LitElement {
|
||||
<div class="section">
|
||||
${this.hass.localize("ui.card.weather.forecast")}:
|
||||
</div>
|
||||
${supportedForecasts.length > 1
|
||||
? html`<mwc-tab-bar
|
||||
.activeIndex=${supportedForecasts.findIndex(
|
||||
(item) => item === this._forecastType
|
||||
)}
|
||||
@MDCTabBar:activated=${this._handleForecastTypeChanged}
|
||||
>
|
||||
${supportedForecasts.map(
|
||||
(forecastType) =>
|
||||
html`<mwc-tab
|
||||
.label=${this.hass!.localize(
|
||||
`ui.card.weather.${forecastType}`
|
||||
)}
|
||||
></mwc-tab>`
|
||||
)}
|
||||
</mwc-tab-bar>`
|
||||
: nothing}
|
||||
${forecast.map((item) =>
|
||||
this._showValue(item.templow) || this._showValue(item.temperature)
|
||||
? html`<div class="flex">
|
||||
@@ -283,12 +319,23 @@ class MoreInfoWeather extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleForecastTypeChanged(ev: CustomEvent): void {
|
||||
this._forecastType = this._supportedForecasts(this.stateObj!)[
|
||||
ev.detail.index
|
||||
];
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-svg-icon {
|
||||
color: var(--paper-item-icon-color);
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
mwc-tab-bar {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 16px 0 8px 0;
|
||||
font-size: 1.2em;
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import "@material/mwc-tab";
|
||||
import "@material/mwc-tab-bar";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
|
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import "../components/ha-logo-svg";
|
||||
|
||||
class HaInitPage extends LitElement {
|
||||
@property({ type: Boolean }) public error = false;
|
||||
@@ -13,36 +14,36 @@ class HaInitPage extends LitElement {
|
||||
private _retryInterval?: number;
|
||||
|
||||
protected render() {
|
||||
return this.error
|
||||
? html`
|
||||
<p>Unable to connect to Home Assistant.</p>
|
||||
<p class="retry-text">
|
||||
Retrying in ${this._retryInSeconds} seconds...
|
||||
</p>
|
||||
<mwc-button @click=${this._retry}>Retry now</mwc-button>
|
||||
${location.host.includes("ui.nabu.casa")
|
||||
? html`
|
||||
<p>
|
||||
It is possible that you are seeing this screen because your
|
||||
Home Assistant is not currently connected. You can ask it to
|
||||
come online from your
|
||||
<a href="https://account.nabucasa.com/"
|
||||
>Nabu Casa account page</a
|
||||
>.
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: html`
|
||||
<div id="progress-indicator-wrapper">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
</div>
|
||||
<div id="loading-text">
|
||||
${this.migration
|
||||
? "Database migration in progress, please wait this might take some time"
|
||||
: "Loading data"}
|
||||
</div>
|
||||
`;
|
||||
return html`<ha-logo-svg></ha-logo-svg>${this.error
|
||||
? html`
|
||||
<p>Unable to connect to Home Assistant.</p>
|
||||
<p class="retry-text">
|
||||
Retrying in ${this._retryInSeconds} seconds...
|
||||
</p>
|
||||
<mwc-button @click=${this._retry}>Retry now</mwc-button>
|
||||
${location.host.includes("ui.nabu.casa")
|
||||
? html`
|
||||
<p>
|
||||
It is possible that you are seeing this screen because your
|
||||
Home Assistant is not currently connected. You can ask it to
|
||||
come online from your
|
||||
<a href="https://account.nabucasa.com/"
|
||||
>Nabu Casa account page</a
|
||||
>.
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: html`
|
||||
<div id="progress-indicator-wrapper">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
</div>
|
||||
<div id="loading-text">
|
||||
${this.migration
|
||||
? "Database migration in progress, please wait this might take some time"
|
||||
: "Loading data"}
|
||||
</div>
|
||||
`}`;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@@ -63,12 +64,15 @@ class HaInitPage extends LitElement {
|
||||
|
||||
protected firstUpdated() {
|
||||
this._showProgressIndicatorTimeout = window.setTimeout(() => {
|
||||
this._showProgressIndicatorTimeout = undefined;
|
||||
import("../components/ha-circular-progress");
|
||||
}, 5000);
|
||||
|
||||
this._retryInterval = window.setInterval(() => {
|
||||
const remainingSeconds = this._retryInSeconds--;
|
||||
if (remainingSeconds <= 0) {
|
||||
clearInterval(this._retryInterval);
|
||||
this._retryInterval = undefined;
|
||||
this._retry();
|
||||
}
|
||||
}, 1000);
|
||||
@@ -86,6 +90,11 @@ class HaInitPage extends LitElement {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
ha-logo-svg {
|
||||
height: 170px;
|
||||
width: 170px;
|
||||
padding: 12px;
|
||||
}
|
||||
#progress-indicator-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@@ -9,15 +9,11 @@ import { HassElement } from "../state/hass-element";
|
||||
import QuickBarMixin from "../state/quick-bar-mixin";
|
||||
import { HomeAssistant, Route } from "../types";
|
||||
import { storeState } from "../util/ha-pref-storage";
|
||||
import {
|
||||
renderLaunchScreenInfoBox,
|
||||
removeLaunchScreen,
|
||||
} from "../util/launch-screen";
|
||||
import { renderLaunchScreen, removeLaunchScreen } from "../util/launch-screen";
|
||||
import {
|
||||
registerServiceWorker,
|
||||
supportsServiceWorker,
|
||||
} from "../util/register-service-worker";
|
||||
import "./ha-init-page";
|
||||
import "./home-assistant-main";
|
||||
|
||||
const useHash = __DEMO__;
|
||||
@@ -39,8 +35,12 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
|
||||
private _haVersion?: string;
|
||||
|
||||
private _error?: boolean;
|
||||
|
||||
private _hiddenTimeout?: number;
|
||||
|
||||
private _renderInitTimeout?: number;
|
||||
|
||||
private _visiblePromiseResolve?: () => void;
|
||||
|
||||
constructor() {
|
||||
@@ -89,6 +89,10 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
) {
|
||||
this.render = this.renderHass;
|
||||
this.update = super.update;
|
||||
if (this._renderInitTimeout) {
|
||||
clearTimeout(this._renderInitTimeout);
|
||||
this._renderInitTimeout = undefined;
|
||||
}
|
||||
removeLaunchScreen();
|
||||
}
|
||||
super.update(changedProps);
|
||||
@@ -139,7 +143,9 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
// Render launch screen info box (loading data / error message)
|
||||
// if Home Assistant is not loaded yet.
|
||||
if (this.render !== this.renderHass) {
|
||||
this._renderInitInfo(false);
|
||||
this._renderInitTimeout = window.setTimeout(() => {
|
||||
this._renderInitInfo();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +159,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
}
|
||||
if (changedProps.has("_databaseMigration")) {
|
||||
if (this.render !== this.renderHass) {
|
||||
this._renderInitInfo(false);
|
||||
this._renderInitInfo();
|
||||
} else if (this._databaseMigration) {
|
||||
// we already removed the launch screen, so we refresh to add it again to show the migration screen
|
||||
location.reload();
|
||||
@@ -233,7 +239,8 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
this._haVersion = conn.haVersion;
|
||||
this.initializeHass(auth, conn);
|
||||
} catch (err: any) {
|
||||
this._renderInitInfo(true);
|
||||
this._error = true;
|
||||
this._renderInitInfo();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,10 +297,15 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _renderInitInfo(error: boolean) {
|
||||
renderLaunchScreenInfoBox(
|
||||
private async _renderInitInfo() {
|
||||
if (this._renderInitTimeout) {
|
||||
clearTimeout(this._renderInitTimeout);
|
||||
}
|
||||
this._renderInitTimeout = undefined;
|
||||
await import("./ha-init-page");
|
||||
renderLaunchScreen(
|
||||
html`<ha-init-page
|
||||
.error=${error}
|
||||
.error=${this._error}
|
||||
.migration=${this._databaseMigration}
|
||||
></ha-init-page>`
|
||||
);
|
||||
|
@@ -15,7 +15,11 @@ import {
|
||||
} from "../common/auth/token_storage";
|
||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||
import { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import {
|
||||
addSearchParam,
|
||||
extractSearchParam,
|
||||
extractSearchParamsObject,
|
||||
} from "../common/url/search-params";
|
||||
import { subscribeOne } from "../common/util/subscribe-one";
|
||||
import "../components/ha-card";
|
||||
import "../components/ha-language-picker";
|
||||
@@ -39,6 +43,8 @@ import "./onboarding-loading";
|
||||
import "./onboarding-welcome";
|
||||
import "./onboarding-welcome-links";
|
||||
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
|
||||
type OnboardingEvent =
|
||||
| {
|
||||
@@ -96,6 +102,27 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
|
||||
@state() private _steps?: OnboardingStep[];
|
||||
|
||||
@state() private _page = extractSearchParam("page");
|
||||
|
||||
private _mobileApp =
|
||||
extractSearchParam("redirect_uri") === "homeassistant://auth-callback";
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
mainWindow.addEventListener("location-changed", this._updatePage);
|
||||
mainWindow.addEventListener("popstate", this._updatePage);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.connectedCallback();
|
||||
mainWindow.removeEventListener("location-changed", this._updatePage);
|
||||
mainWindow.removeEventListener("popstate", this._updatePage);
|
||||
}
|
||||
|
||||
private _updatePage = () => {
|
||||
this._page = extractSearchParam("page");
|
||||
};
|
||||
|
||||
protected render() {
|
||||
return html`<mwc-linear-progress
|
||||
.progress=${this._progress}
|
||||
@@ -103,9 +130,10 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
<ha-card>
|
||||
<div class="card-content">${this._renderStep()}</div>
|
||||
</ha-card>
|
||||
${this._init
|
||||
${this._init && !this._restoring
|
||||
? html`<onboarding-welcome-links
|
||||
.localize=${this.localize}
|
||||
.mobileApp=${this._mobileApp}
|
||||
></onboarding-welcome-links>`
|
||||
: nothing}
|
||||
<div class="footer">
|
||||
@@ -125,6 +153,14 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
}
|
||||
|
||||
private _renderStep() {
|
||||
if (this._restoring) {
|
||||
return html`<onboarding-restore-backup
|
||||
.hass=${this.hass}
|
||||
.localize=${this.localize}
|
||||
>
|
||||
</onboarding-restore-backup>`;
|
||||
}
|
||||
|
||||
if (this._init) {
|
||||
return html`<onboarding-welcome
|
||||
.localize=${this.localize}
|
||||
@@ -133,11 +169,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
></onboarding-welcome>`;
|
||||
}
|
||||
|
||||
if (this._restoring) {
|
||||
return html`<onboarding-restore-backup .localize=${this.localize}>
|
||||
</onboarding-restore-backup>`;
|
||||
}
|
||||
|
||||
const step = this._curStep()!;
|
||||
|
||||
if (this._loading || !step) {
|
||||
@@ -195,6 +226,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("_page")) {
|
||||
this._restoring = this._page === "restore_backup";
|
||||
if (this._page === null && this._steps && !this._steps[0].done) {
|
||||
this._init = true;
|
||||
}
|
||||
}
|
||||
if (changedProps.has("language")) {
|
||||
document.querySelector("html")!.setAttribute("lang", this.language!);
|
||||
}
|
||||
@@ -312,6 +349,10 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
this._restoring = stepResult.result.restore;
|
||||
if (!this._restoring) {
|
||||
this._progress = 0.25;
|
||||
} else {
|
||||
navigate(
|
||||
`${location.pathname}?${addSearchParam({ page: "restore_backup" })}`
|
||||
);
|
||||
}
|
||||
} else if (stepResult.type === "user") {
|
||||
const result = stepResult.result as OnboardingResponses["user"];
|
||||
|
@@ -1,42 +1,55 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload";
|
||||
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
|
||||
import "../../hassio/src/components/hassio-upload-backup";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-ansi-to-html";
|
||||
import "../components/ha-card";
|
||||
import { fetchInstallationType } from "../data/onboarding";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./onboarding-loading";
|
||||
import { onBoardingStyles } from "./styles";
|
||||
import { removeSearchParam } from "../common/url/search-params";
|
||||
import { navigate } from "../common/navigate";
|
||||
|
||||
@customElement("onboarding-restore-backup")
|
||||
class OnboardingRestoreBackup extends LitElement {
|
||||
@property() public localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
@property() public language!: string;
|
||||
|
||||
@state() public _restoring = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return this._restoring
|
||||
? html`<h1>
|
||||
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
|
||||
</h1>
|
||||
<onboarding-loading></onboarding-loading>`
|
||||
: html`
|
||||
<h1>${this.localize("ui.panel.page-onboarding.restore.header")}</h1>
|
||||
<ha-button unelevated @click=${this._uploadBackup}>
|
||||
${this.localize("ui.panel.page-onboarding.restore.upload_backup")}
|
||||
</ha-button>
|
||||
`;
|
||||
return html`${this._restoring
|
||||
? html`<h1>
|
||||
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
|
||||
</h1>
|
||||
<onboarding-loading></onboarding-loading>`
|
||||
: html` <h1>
|
||||
${this.localize("ui.panel.page-onboarding.restore.header")}
|
||||
</h1>
|
||||
<hassio-upload-backup
|
||||
@backup-uploaded=${this._backupUploaded}
|
||||
.hass=${this.hass}
|
||||
></hassio-upload-backup>`}
|
||||
<div class="footer">
|
||||
<mwc-button @click=${this._back} .disabled=${this._restoring}>
|
||||
${this.localize("ui.panel.page-onboarding.back")}
|
||||
</mwc-button>
|
||||
</div> `;
|
||||
}
|
||||
|
||||
private _uploadBackup(): void {
|
||||
showBackupUploadDialog(this, {
|
||||
showBackup: (slug: string) => this._showBackupDialog(slug),
|
||||
onboarding: true,
|
||||
});
|
||||
private _back(): void {
|
||||
navigate(`${location.pathname}?${removeSearchParam("page")}`);
|
||||
}
|
||||
|
||||
private _backupUploaded(ev) {
|
||||
const backup = ev.detail.backup;
|
||||
this._showBackupDialog(backup.slug);
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
@@ -76,6 +89,13 @@ class OnboardingRestoreBackup extends LitElement {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
hassio-upload-backup {
|
||||
width: 100%;
|
||||
}
|
||||
.footer {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -94,6 +94,7 @@ class OnboardingWelcomeLink extends LitElement {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
padding: 32px 16px;
|
||||
height: 100%;
|
||||
}
|
||||
ha-svg-icon {
|
||||
color: var(--text-primary-color);
|
||||
|
@@ -1,5 +1,12 @@
|
||||
import { mdiAccountGroup, mdiFileDocument, mdiTabletCellphone } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-card";
|
||||
@@ -14,6 +21,8 @@ class OnboardingWelcomeLinks extends LitElement {
|
||||
|
||||
@property() public localize!: LocalizeFunc;
|
||||
|
||||
@property({ type: Boolean }) public mobileApp!: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<a
|
||||
target="_blank"
|
||||
@@ -34,13 +43,17 @@ class OnboardingWelcomeLinks extends LitElement {
|
||||
.label=${this.localize("ui.panel.page-onboarding.welcome.community")}
|
||||
>
|
||||
</onboarding-welcome-link>
|
||||
<onboarding-welcome-link
|
||||
class="app"
|
||||
@click=${this._openApp}
|
||||
.iconPath=${mdiTabletCellphone}
|
||||
.label=${this.localize("ui.panel.page-onboarding.welcome.download_app")}
|
||||
>
|
||||
</onboarding-welcome-link>`;
|
||||
${this.mobileApp
|
||||
? nothing
|
||||
: html`<onboarding-welcome-link
|
||||
class="app"
|
||||
@click=${this._openApp}
|
||||
.iconPath=${mdiTabletCellphone}
|
||||
.label=${this.localize(
|
||||
"ui.panel.page-onboarding.welcome.download_app"
|
||||
)}
|
||||
>
|
||||
</onboarding-welcome-link>`}`;
|
||||
}
|
||||
|
||||
private _openCommunity(): void {
|
||||
|
@@ -80,9 +80,7 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
name: domainToName(this.hass.localize, domain),
|
||||
}));
|
||||
await this.hass.loadBackendTranslation("application_credentials");
|
||||
if (this._domain) {
|
||||
this._updateDescription();
|
||||
}
|
||||
this._updateDescription();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -265,11 +263,15 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
}
|
||||
|
||||
private async _updateDescription() {
|
||||
if (!this._domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.hass.loadBackendTranslation(
|
||||
"application_credentials",
|
||||
this._domain
|
||||
);
|
||||
const info = this._config!.integrations[this._domain!];
|
||||
const info = this._config!.integrations[this._domain];
|
||||
this._description = this.hass.localize(
|
||||
`component.${this._domain}.application_credentials.description`,
|
||||
info.description_placeholders
|
||||
|
@@ -62,17 +62,16 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
),
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (_, entry: ApplicationCredential) => html`${entry.name}`,
|
||||
template: (entry) => html`${entry.name}`,
|
||||
},
|
||||
clientId: {
|
||||
client_id: {
|
||||
title: localize(
|
||||
"ui.panel.config.application_credentials.picker.headers.client_id"
|
||||
),
|
||||
width: "30%",
|
||||
direction: "asc",
|
||||
hidden: narrow,
|
||||
template: (_, entry: ApplicationCredential) =>
|
||||
html`${entry.client_id}`,
|
||||
template: (entry) => html`${entry.client_id}`,
|
||||
},
|
||||
application: {
|
||||
title: localize(
|
||||
@@ -81,7 +80,7 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
sortable: true,
|
||||
width: "30%",
|
||||
direction: "asc",
|
||||
template: (_, entry) => html`${domainToName(localize, entry.domain)}`,
|
||||
template: (entry) => html`${domainToName(localize, entry.domain)}`,
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import {
|
||||
@@ -25,7 +26,6 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { consume } from "@lit-labs/context";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
@@ -40,6 +40,7 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import { ACTION_TYPES, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
|
||||
import { AutomationClipboard } from "../../../../data/automation";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
import {
|
||||
Action,
|
||||
@@ -70,19 +71,20 @@ import "./types/ha-automation-action-service";
|
||||
import "./types/ha-automation-action-stop";
|
||||
import "./types/ha-automation-action-wait_for_trigger";
|
||||
import "./types/ha-automation-action-wait_template";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
|
||||
export const getType = (action: Action | undefined) => {
|
||||
if (!action) {
|
||||
return undefined;
|
||||
}
|
||||
if ("service" in action || "scene" in action) {
|
||||
return getActionType(action);
|
||||
return getActionType(action) as "activate_scene" | "service" | "play_media";
|
||||
}
|
||||
if (["and", "or", "not"].some((key) => key in action)) {
|
||||
return "condition";
|
||||
return "condition" as const;
|
||||
}
|
||||
return Object.keys(ACTION_TYPES).find((option) => option in action);
|
||||
return Object.keys(ACTION_TYPES).find(
|
||||
(option) => option in action
|
||||
) as keyof typeof ACTION_TYPES;
|
||||
};
|
||||
|
||||
export interface ActionElement extends LitElement {
|
||||
|
@@ -3,41 +3,42 @@ import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiContentPaste,
|
||||
mdiDrag,
|
||||
mdiPlus,
|
||||
mdiContentPaste,
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { SortableEvent } from "sortablejs";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import type { HaSelect } from "../../../../components/ha-select";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { ACTION_TYPES } from "../../../../data/action";
|
||||
import { Action } from "../../../../data/script";
|
||||
import { AutomationClipboard } from "../../../../data/automation";
|
||||
import { Action } from "../../../../data/script";
|
||||
import { sortableStyles } from "../../../../resources/ha-sortable-style";
|
||||
import {
|
||||
loadSortable,
|
||||
SortableInstance,
|
||||
loadSortable,
|
||||
} from "../../../../resources/sortable.ondemand";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { getType } from "./ha-automation-action-row";
|
||||
import { Entries, HomeAssistant } from "../../../../types";
|
||||
import type HaAutomationActionRow from "./ha-automation-action-row";
|
||||
import { getType } from "./ha-automation-action-row";
|
||||
import "./types/ha-automation-action-activate_scene";
|
||||
import "./types/ha-automation-action-choose";
|
||||
import "./types/ha-automation-action-condition";
|
||||
@@ -52,7 +53,6 @@ import "./types/ha-automation-action-service";
|
||||
import "./types/ha-automation-action-stop";
|
||||
import "./types/ha-automation-action-wait_for_trigger";
|
||||
import "./types/ha-automation-action-wait_template";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
|
||||
const PASTE_VALUE = "__paste__";
|
||||
|
||||
@@ -174,9 +174,9 @@ export default class HaAutomationAction extends LitElement {
|
||||
"ui.panel.config.automation.editor.actions.paste"
|
||||
)}
|
||||
(${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.type.${getType(
|
||||
this._clipboard.action
|
||||
)}.label`
|
||||
`ui.panel.config.automation.editor.actions.type.${
|
||||
getType(this._clipboard.action) || "unknown"
|
||||
}.label`
|
||||
)})
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentPaste}></ha-svg-icon
|
||||
></mwc-list-item>`
|
||||
@@ -333,7 +333,7 @@ export default class HaAutomationAction extends LitElement {
|
||||
|
||||
private _processedTypes = memoizeOne(
|
||||
(localize: LocalizeFunc): [string, string, string][] =>
|
||||
Object.entries(ACTION_TYPES)
|
||||
(Object.entries(ACTION_TYPES) as Entries<typeof ACTION_TYPES>)
|
||||
.map(
|
||||
([action, icon]) =>
|
||||
[
|
||||
|
@@ -8,7 +8,7 @@ import "../../../../../components/ha-select";
|
||||
import type { HaSelect } from "../../../../../components/ha-select";
|
||||
import type { Condition } from "../../../../../data/automation";
|
||||
import { CONDITION_TYPES } from "../../../../../data/condition";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { Entries, HomeAssistant } from "../../../../../types";
|
||||
import "../../condition/ha-automation-condition-editor";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
@@ -55,7 +55,7 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
|
||||
private _processedTypes = memoizeOne(
|
||||
(localize: LocalizeFunc): [string, string, string][] =>
|
||||
Object.entries(CONDITION_TYPES)
|
||||
(Object.entries(CONDITION_TYPES) as Entries<typeof CONDITION_TYPES>)
|
||||
.map(
|
||||
([condition, icon]) =>
|
||||
[
|
||||
|
@@ -28,12 +28,13 @@ import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
} from "../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { Entries, HomeAssistant } from "../../../../types";
|
||||
import "./ha-automation-condition-row";
|
||||
import type HaAutomationConditionRow from "./ha-automation-condition-row";
|
||||
// Uncommenting these and this element doesn't load
|
||||
// import "./types/ha-automation-condition-not";
|
||||
// import "./types/ha-automation-condition-or";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { HaSelect } from "../../../../components/ha-select";
|
||||
@@ -52,7 +53,6 @@ import "./types/ha-automation-condition-template";
|
||||
import "./types/ha-automation-condition-time";
|
||||
import "./types/ha-automation-condition-trigger";
|
||||
import "./types/ha-automation-condition-zone";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
|
||||
const PASTE_VALUE = "__paste__";
|
||||
|
||||
@@ -364,7 +364,7 @@ export default class HaAutomationCondition extends LitElement {
|
||||
|
||||
private _processedTypes = memoizeOne(
|
||||
(localize: LocalizeFunc): [string, string, string][] =>
|
||||
Object.entries(CONDITION_TYPES)
|
||||
(Object.entries(CONDITION_TYPES) as Entries<typeof CONDITION_TYPES>)
|
||||
.map(
|
||||
([condition, icon]) =>
|
||||
[
|
||||
|
@@ -53,11 +53,6 @@ export class HaZoneCondition extends LitElement {
|
||||
allow-custom-entity
|
||||
.includeDomains=${includeDomains}
|
||||
></ha-entity-picker>
|
||||
<label id="eventlabel">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.zone.event"
|
||||
)}
|
||||
</label>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -49,6 +49,8 @@ import {
|
||||
showAutomationEditor,
|
||||
triggerAutomationActions,
|
||||
} from "../../../data/automation";
|
||||
import { validateConfig } from "../../../data/config";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { fetchEntityRegistry } from "../../../data/entity_registry";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -57,15 +59,13 @@ import {
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { Entries, HomeAssistant, Route } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "../ha-config-section";
|
||||
import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
|
||||
import { showAutomationRenameDialog } from "./automation-rename-dialog/show-dialog-automation-rename";
|
||||
import "./blueprint-automation-editor";
|
||||
import "./manual-automation-editor";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { validateConfig } from "../../../data/config";
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -489,7 +489,9 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
condition: this._config.condition,
|
||||
action: this._config.action,
|
||||
});
|
||||
this._validationErrors = Object.entries(validation).map(([key, value]) =>
|
||||
this._validationErrors = (
|
||||
Object.entries(validation) as Entries<typeof validation>
|
||||
).map(([key, value]) =>
|
||||
value.valid
|
||||
? ""
|
||||
: html`${this.hass.localize(
|
||||
|
@@ -55,6 +55,12 @@ import { findRelated } from "../../../data/search";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
|
||||
type AutomationItem = AutomationEntity & {
|
||||
name: string;
|
||||
last_triggered?: string | undefined;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
@customElement("ha-automation-picker")
|
||||
class HaAutomationPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -79,7 +85,7 @@ class HaAutomationPicker extends LitElement {
|
||||
(
|
||||
automations: AutomationEntity[],
|
||||
filteredAutomations?: string[] | null
|
||||
) => {
|
||||
): AutomationItem[] => {
|
||||
if (filteredAutomations === null) {
|
||||
return [];
|
||||
}
|
||||
@@ -100,14 +106,14 @@ class HaAutomationPicker extends LitElement {
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean, _locale): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
const columns: DataTableColumnContainer<AutomationItem> = {
|
||||
icon: {
|
||||
title: "",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.automation.picker.headers.state"
|
||||
),
|
||||
type: "icon",
|
||||
template: (_, automation) =>
|
||||
template: (automation) =>
|
||||
html`<ha-state-icon
|
||||
.state=${automation}
|
||||
style=${styleMap({
|
||||
@@ -128,12 +134,12 @@ class HaAutomationPicker extends LitElement {
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: narrow
|
||||
? (name, automation: any) => {
|
||||
? (automation) => {
|
||||
const date = new Date(automation.attributes.last_triggered);
|
||||
const now = new Date();
|
||||
const dayDifference = differenceInDays(now, date);
|
||||
return html`
|
||||
${name}
|
||||
${automation.name}
|
||||
<div class="secondary">
|
||||
${this.hass.localize("ui.card.automation.last_triggered")}:
|
||||
${automation.attributes.last_triggered
|
||||
@@ -156,20 +162,17 @@ class HaAutomationPicker extends LitElement {
|
||||
sortable: true,
|
||||
width: "20%",
|
||||
title: this.hass.localize("ui.card.automation.last_triggered"),
|
||||
template: (last_triggered) => {
|
||||
const date = new Date(last_triggered);
|
||||
template: (automation) => {
|
||||
if (!automation.last_triggered) {
|
||||
return this.hass.localize("ui.components.relative_time.never");
|
||||
}
|
||||
const date = new Date(automation.last_triggered);
|
||||
const now = new Date();
|
||||
const dayDifference = differenceInDays(now, date);
|
||||
return html`
|
||||
${last_triggered
|
||||
? dayDifference > 3
|
||||
? formatShortDateTime(
|
||||
date,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)
|
||||
: relativeTime(date, this.hass.locale)
|
||||
: this.hass.localize("ui.components.relative_time.never")}
|
||||
${dayDifference > 3
|
||||
? formatShortDateTime(date, this.hass.locale, this.hass.config)
|
||||
: relativeTime(date, this.hass.locale)}
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -178,8 +181,8 @@ class HaAutomationPicker extends LitElement {
|
||||
columns.disabled = this.narrow
|
||||
? {
|
||||
title: "",
|
||||
template: (disabled: boolean) =>
|
||||
disabled
|
||||
template: (automation) =>
|
||||
automation.disabled
|
||||
? html`
|
||||
<simple-tooltip animation-delay="0" position="left">
|
||||
${this.hass.localize(
|
||||
@@ -196,8 +199,8 @@ class HaAutomationPicker extends LitElement {
|
||||
: {
|
||||
width: "20%",
|
||||
title: "",
|
||||
template: (disabled: boolean) =>
|
||||
disabled
|
||||
template: (automation) =>
|
||||
automation.disabled
|
||||
? html`
|
||||
<ha-chip>
|
||||
${this.hass.localize(
|
||||
@@ -212,7 +215,7 @@ class HaAutomationPicker extends LitElement {
|
||||
title: "",
|
||||
width: this.narrow ? undefined : "10%",
|
||||
type: "overflow-menu",
|
||||
template: (_: string, automation: any) => html`
|
||||
template: (automation) => html`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
narrow
|
||||
|
@@ -1,273 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-circular-progress";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../components/ha-textfield";
|
||||
import type { AutomationConfig } from "../../../../data/automation";
|
||||
import { convertThingTalk } from "../../../../data/cloud";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "./ha-thingtalk-placeholders";
|
||||
import type { PlaceholderValues } from "./ha-thingtalk-placeholders";
|
||||
import type { ThingtalkDialogParams } from "./show-dialog-thingtalk";
|
||||
|
||||
export interface Placeholder {
|
||||
name: string;
|
||||
index: number;
|
||||
fields: string[];
|
||||
domains: string[];
|
||||
device_classes?: string[];
|
||||
}
|
||||
|
||||
export interface PlaceholderContainer {
|
||||
[key: string]: Placeholder[];
|
||||
}
|
||||
|
||||
@customElement("ha-dialog-thinktalk")
|
||||
class DialogThingtalk extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _params?: ThingtalkDialogParams;
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@state() private _placeholders?: PlaceholderContainer;
|
||||
|
||||
@query("#input") private _input?: HaTextField;
|
||||
|
||||
private _value?: string;
|
||||
|
||||
private _config!: Partial<AutomationConfig>;
|
||||
|
||||
public async showDialog(params: ThingtalkDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
if (params.input) {
|
||||
this._value = params.input;
|
||||
await this.updateComplete;
|
||||
this._generate();
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._placeholders = undefined;
|
||||
this._params = undefined;
|
||||
if (this._input) {
|
||||
this._input.value = "";
|
||||
}
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public closeInitDialog() {
|
||||
if (this._placeholders) {
|
||||
return;
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
if (this._placeholders) {
|
||||
return html`
|
||||
<ha-thingtalk-placeholders
|
||||
.hass=${this.hass}
|
||||
.placeholders=${this._placeholders}
|
||||
.skip=${this._skip}
|
||||
@closed=${this.closeDialog}
|
||||
@placeholders-filled=${this._handlePlaceholders}
|
||||
>
|
||||
</ha-thingtalk-placeholders>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeInitDialog}
|
||||
.heading=${this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.task_selection.header`
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.task_selection.introduction`
|
||||
)}<br /><br />
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.task_selection.language_note`
|
||||
)}<br /><br />
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.task_selection.for_example`
|
||||
)}
|
||||
<ul @click=${this._handleExampleClick}>
|
||||
<li>
|
||||
<button class="link">
|
||||
Turn off the lights when I leave home
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="link">
|
||||
Turn on the lights when the sun is set
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="link">
|
||||
Notify me if the door opens and I am not at home
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="link">
|
||||
Turn the light on when motion is detected
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ha-textfield
|
||||
id="input"
|
||||
label="What should this automation do?"
|
||||
.value=${this._value}
|
||||
autofocus
|
||||
@keyup=${this._handleKeyUp}
|
||||
></ha-textfield>
|
||||
<a
|
||||
href="https://almond.stanford.edu/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="attribution"
|
||||
>Powered by Almond</a
|
||||
>
|
||||
</div>
|
||||
<mwc-button class="left" @click=${this._skip} slot="secondaryAction">
|
||||
${this.hass.localize(`ui.common.skip`)}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
@click=${this._generate}
|
||||
.disabled=${this._submitting}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this._submitting
|
||||
? html`<ha-circular-progress
|
||||
active
|
||||
size="small"
|
||||
title="Creating your automation..."
|
||||
></ha-circular-progress>`
|
||||
: ""}
|
||||
${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)}
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _generate() {
|
||||
this._value = this._input!.value as string;
|
||||
if (!this._value) {
|
||||
this._error = this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.task_selection.error_empty`
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._submitting = true;
|
||||
let config: Partial<AutomationConfig>;
|
||||
let placeholders: PlaceholderContainer;
|
||||
try {
|
||||
const result = await convertThingTalk(this.hass, this._value);
|
||||
config = result.config;
|
||||
placeholders = result.placeholders;
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
this._submitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._submitting = false;
|
||||
|
||||
if (!Object.keys(config).length) {
|
||||
this._error = this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.task_selection.error_unsupported`
|
||||
);
|
||||
} else if (Object.keys(placeholders).length) {
|
||||
this._config = config;
|
||||
this._placeholders = placeholders;
|
||||
} else {
|
||||
this._sendConfig(this._value, config);
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePlaceholders(ev: CustomEvent) {
|
||||
const placeholderValues = ev.detail.value as PlaceholderValues;
|
||||
Object.entries(placeholderValues).forEach(([type, values]) => {
|
||||
Object.entries(values).forEach(([index, placeholder]) => {
|
||||
const devices = Object.values(placeholder);
|
||||
if (devices.length === 1) {
|
||||
Object.entries(devices[0]).forEach(([field, value]) => {
|
||||
this._config[type][index][field] = value;
|
||||
});
|
||||
return;
|
||||
}
|
||||
const automation = { ...this._config[type][index] };
|
||||
const newAutomations: any[] = [];
|
||||
devices.forEach((fields) => {
|
||||
const newAutomation = { ...automation };
|
||||
Object.entries(fields).forEach(([field, value]) => {
|
||||
newAutomation[field] = value;
|
||||
});
|
||||
newAutomations.push(newAutomation);
|
||||
});
|
||||
this._config[type].splice(index, 1, ...newAutomations);
|
||||
});
|
||||
});
|
||||
this._sendConfig(this._value, this._config);
|
||||
}
|
||||
|
||||
private _sendConfig(input, config) {
|
||||
this._params!.callback({ alias: input, ...config });
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _skip = () => {
|
||||
this._params!.callback(undefined);
|
||||
this.closeDialog();
|
||||
};
|
||||
|
||||
private _handleKeyUp(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter") {
|
||||
this._generate();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleExampleClick(ev: Event) {
|
||||
this._input!.value = (ev.target as HTMLAnchorElement).innerText;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
max-width: 500px;
|
||||
}
|
||||
mwc-button.left {
|
||||
margin-right: auto;
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.attribution {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-dialog-thinktalk": DialogThingtalk;
|
||||
}
|
||||
}
|
@@ -1,483 +0,0 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { applyPatch, getPath } from "../../../../common/util/patch";
|
||||
import "../../../../components/device/ha-area-devices-picker";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../../../data/area_registry";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../../../data/device_registry";
|
||||
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
|
||||
import { domainToName } from "../../../../data/integration";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { Placeholder, PlaceholderContainer } from "./dialog-thingtalk";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"placeholders-filled": { value: PlaceholderValues };
|
||||
}
|
||||
}
|
||||
|
||||
export interface PlaceholderValues {
|
||||
[key: string]: {
|
||||
[index: number]: {
|
||||
[index: number]: { device_id?: string; entity_id?: string };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ExtraInfo {
|
||||
[key: string]: {
|
||||
[index: number]: {
|
||||
[index: number]: {
|
||||
area_id?: string;
|
||||
device_ids?: string[];
|
||||
manualEntity: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface DeviceEntitiesLookup {
|
||||
[deviceId: string]: string[];
|
||||
}
|
||||
|
||||
@customElement("ha-thingtalk-placeholders")
|
||||
export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public opened!: boolean;
|
||||
|
||||
public skip!: () => void;
|
||||
|
||||
@property() public placeholders!: PlaceholderContainer;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _deviceEntityLookup: DeviceEntitiesLookup = {};
|
||||
|
||||
@state() private _extraInfo: ExtraInfo = {};
|
||||
|
||||
@state() private _placeholderValues: PlaceholderValues = {};
|
||||
|
||||
private _devices?: DeviceRegistryEntry[];
|
||||
|
||||
private _areas?: AreaRegistryEntry[];
|
||||
|
||||
private _search = false;
|
||||
|
||||
public hassSubscribe() {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
||||
for (const entity of entries) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.device_id in this._deviceEntityLookup)) {
|
||||
this._deviceEntityLookup[entity.device_id] = [];
|
||||
}
|
||||
if (
|
||||
!this._deviceEntityLookup[entity.device_id].includes(
|
||||
entity.entity_id
|
||||
)
|
||||
) {
|
||||
this._deviceEntityLookup[entity.device_id].push(entity.entity_id);
|
||||
}
|
||||
}
|
||||
}),
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
this._devices = devices;
|
||||
this._searchNames();
|
||||
}),
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
this._areas = areas;
|
||||
this._searchNames();
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("placeholders")) {
|
||||
this._search = true;
|
||||
this._searchNames();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
.heading=${this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.link_devices.header`
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
${Object.entries(this.placeholders).map(
|
||||
([type, placeholders]) => html`
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${type}s.name`
|
||||
)}:
|
||||
</h3>
|
||||
${placeholders.map((placeholder) => {
|
||||
if (placeholder.fields.includes("device_id")) {
|
||||
const extraInfo = getPath(this._extraInfo, [
|
||||
type,
|
||||
placeholder.index,
|
||||
]);
|
||||
return html`
|
||||
<ha-area-devices-picker
|
||||
.type=${type}
|
||||
.placeholder=${placeholder}
|
||||
@value-changed=${this._devicePicked}
|
||||
.hass=${this.hass}
|
||||
.area=${extraInfo ? extraInfo.area_id : undefined}
|
||||
.devices=${extraInfo && extraInfo.device_ids
|
||||
? extraInfo.device_ids
|
||||
: undefined}
|
||||
.includeDomains=${placeholder.domains}
|
||||
.includeDeviceClasses=${placeholder.device_classes}
|
||||
.label=${this._getLabel(
|
||||
placeholder.domains,
|
||||
placeholder.device_classes
|
||||
)}
|
||||
></ha-area-devices-picker>
|
||||
${extraInfo && extraInfo.manualEntity
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.link_devices.ambiguous_entities`
|
||||
)}
|
||||
</h3>
|
||||
${Object.keys(extraInfo.manualEntity).map(
|
||||
(idx) => html`
|
||||
<ha-entity-picker
|
||||
id="device-entity-picker"
|
||||
.type=${type}
|
||||
.placeholder=${placeholder}
|
||||
.index=${idx}
|
||||
@change=${this._entityPicked}
|
||||
.includeDomains=${placeholder.domains}
|
||||
.includeDeviceClasses=${placeholder.device_classes}
|
||||
.hass=${this.hass}
|
||||
.label=${`${this._getLabel(
|
||||
placeholder.domains,
|
||||
placeholder.device_classes
|
||||
)} of device ${this._getDeviceName(
|
||||
getPath(this._placeholderValues, [
|
||||
type,
|
||||
placeholder.index,
|
||||
idx,
|
||||
"device_id",
|
||||
])
|
||||
)}`}
|
||||
.entityFilter=${(entityState: HassEntity) => {
|
||||
const devId =
|
||||
this._placeholderValues[type][
|
||||
placeholder.index
|
||||
][idx].device_id;
|
||||
return this._deviceEntityLookup[
|
||||
devId
|
||||
].includes(entityState.entity_id);
|
||||
}}
|
||||
></ha-entity-picker>
|
||||
`
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
if (placeholder.fields.includes("entity_id")) {
|
||||
return html`
|
||||
<ha-entity-picker
|
||||
.type=${type}
|
||||
.placeholder=${placeholder}
|
||||
@change=${this._entityPicked}
|
||||
.includeDomains=${placeholder.domains}
|
||||
.includeDeviceClasses=${placeholder.device_classes}
|
||||
.hass=${this.hass}
|
||||
.label=${this._getLabel(
|
||||
placeholder.domains,
|
||||
placeholder.device_classes
|
||||
)}
|
||||
></ha-entity-picker>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<div class="error">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.thingtalk.link_devices.unknown_placeholder`
|
||||
)}<br />
|
||||
${placeholder.domains}<br />
|
||||
${placeholder.fields.map((field) => html` ${field}<br /> `)}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<mwc-button @click=${this.skip} slot="secondaryAction">
|
||||
${this.hass.localize(`ui.common.skip`)}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
@click=${this._done}
|
||||
.disabled=${!this._isDone}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)}
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getDeviceName(deviceId: string): string {
|
||||
if (!this._devices) {
|
||||
return "";
|
||||
}
|
||||
const foundDevice = this._devices.find((device) => device.id === deviceId);
|
||||
if (!foundDevice) {
|
||||
return "";
|
||||
}
|
||||
return foundDevice.name_by_user || foundDevice.name || "";
|
||||
}
|
||||
|
||||
private _searchNames() {
|
||||
if (!this._search || !this._areas || !this._devices) {
|
||||
return;
|
||||
}
|
||||
this._search = false;
|
||||
Object.entries(this.placeholders).forEach(([type, placeholders]) =>
|
||||
placeholders.forEach((placeholder) => {
|
||||
if (!placeholder.name) {
|
||||
return;
|
||||
}
|
||||
const name = placeholder.name;
|
||||
const foundArea = this._areas!.find((area) =>
|
||||
area.name.toLowerCase().includes(name)
|
||||
);
|
||||
if (foundArea) {
|
||||
applyPatch(
|
||||
this._extraInfo,
|
||||
[type, placeholder.index, "area_id"],
|
||||
foundArea.area_id
|
||||
);
|
||||
this.requestUpdate("_extraInfo");
|
||||
return;
|
||||
}
|
||||
const foundDevices = this._devices!.filter((device) => {
|
||||
const deviceName = device.name_by_user || device.name;
|
||||
if (!deviceName) {
|
||||
return false;
|
||||
}
|
||||
return deviceName.toLowerCase().includes(name);
|
||||
});
|
||||
if (foundDevices.length) {
|
||||
applyPatch(
|
||||
this._extraInfo,
|
||||
[type, placeholder.index, "device_ids"],
|
||||
foundDevices.map((device) => device.id)
|
||||
);
|
||||
this.requestUpdate("_extraInfo");
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private get _isDone(): boolean {
|
||||
return Object.entries(this.placeholders).every(([type, placeholders]) =>
|
||||
placeholders.every((placeholder) =>
|
||||
placeholder.fields.every((field) => {
|
||||
const entries: {
|
||||
[key: number]: { device_id?: string; entity_id?: string };
|
||||
} = getPath(this._placeholderValues, [type, placeholder.index]);
|
||||
if (!entries) {
|
||||
return false;
|
||||
}
|
||||
const values = Object.values(entries);
|
||||
return values.every(
|
||||
(entry) => entry[field] !== undefined && entry[field] !== ""
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _getLabel(domains: string[], deviceClasses?: string[]) {
|
||||
return `${domains
|
||||
.map((domain) => domainToName(this.hass.localize, domain))
|
||||
.join(", ")}${
|
||||
deviceClasses ? ` of type ${deviceClasses.join(", ")}` : ""
|
||||
}`;
|
||||
}
|
||||
|
||||
private _devicePicked(ev: CustomEvent): void {
|
||||
const value: string[] = ev.detail.value;
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
const target = ev.target as any;
|
||||
const placeholder = target.placeholder as Placeholder;
|
||||
const type = target.type;
|
||||
|
||||
let oldValues = getPath(this._placeholderValues, [type, placeholder.index]);
|
||||
if (oldValues) {
|
||||
oldValues = Object.values(oldValues);
|
||||
}
|
||||
const oldExtraInfo = getPath(this._extraInfo, [type, placeholder.index]);
|
||||
|
||||
if (this._placeholderValues[type]) {
|
||||
delete this._placeholderValues[type][placeholder.index];
|
||||
}
|
||||
|
||||
if (this._extraInfo[type]) {
|
||||
delete this._extraInfo[type][placeholder.index];
|
||||
}
|
||||
|
||||
if (!value.length) {
|
||||
this.requestUpdate("_placeholderValues");
|
||||
return;
|
||||
}
|
||||
|
||||
value.forEach((deviceId, index) => {
|
||||
let oldIndex;
|
||||
if (oldValues) {
|
||||
const oldDevice = oldValues.find((oldVal, idx) => {
|
||||
oldIndex = idx;
|
||||
return oldVal.device_id === deviceId;
|
||||
});
|
||||
|
||||
if (oldDevice) {
|
||||
applyPatch(
|
||||
this._placeholderValues,
|
||||
[type, placeholder.index, index],
|
||||
oldDevice
|
||||
);
|
||||
if (oldExtraInfo) {
|
||||
applyPatch(
|
||||
this._extraInfo,
|
||||
[type, placeholder.index, index],
|
||||
oldExtraInfo[oldIndex]
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
applyPatch(
|
||||
this._placeholderValues,
|
||||
[type, placeholder.index, index, "device_id"],
|
||||
deviceId
|
||||
);
|
||||
|
||||
if (!placeholder.fields.includes("entity_id")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const devEntities = this._deviceEntityLookup[deviceId];
|
||||
|
||||
const entities = devEntities.filter((eid) => {
|
||||
if (placeholder.device_classes) {
|
||||
const stateObj = this.hass.states[eid];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
placeholder.domains.includes(computeDomain(eid)) &&
|
||||
stateObj.attributes.device_class &&
|
||||
placeholder.device_classes.includes(
|
||||
stateObj.attributes.device_class
|
||||
)
|
||||
);
|
||||
}
|
||||
return placeholder.domains.includes(computeDomain(eid));
|
||||
});
|
||||
if (entities.length === 0) {
|
||||
// Should not happen because we filter the device picker on domain
|
||||
this._error = `No ${placeholder.domains
|
||||
.map((domain) => domainToName(this.hass.localize, domain))
|
||||
.join(", ")} entities found in this device.`;
|
||||
} else if (entities.length === 1) {
|
||||
applyPatch(
|
||||
this._placeholderValues,
|
||||
[type, placeholder.index, index, "entity_id"],
|
||||
entities[0]
|
||||
);
|
||||
this.requestUpdate("_placeholderValues");
|
||||
} else {
|
||||
delete this._placeholderValues[type][placeholder.index][index]
|
||||
.entity_id;
|
||||
applyPatch(
|
||||
this._extraInfo,
|
||||
[type, placeholder.index, "manualEntity", index],
|
||||
true
|
||||
);
|
||||
this.requestUpdate("_placeholderValues");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _entityPicked(ev: Event): void {
|
||||
const target = ev.target as any;
|
||||
const placeholder = target.placeholder as Placeholder;
|
||||
const value = target.value;
|
||||
const type = target.type;
|
||||
const index = target.index || 0;
|
||||
applyPatch(
|
||||
this._placeholderValues,
|
||||
[type, placeholder.index, index, "entity_id"],
|
||||
value
|
||||
);
|
||||
this.requestUpdate("_placeholderValues");
|
||||
}
|
||||
|
||||
private _done(): void {
|
||||
fireEvent(this, "placeholders-filled", { value: this._placeholderValues });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
max-width: 500px;
|
||||
}
|
||||
mwc-button.left {
|
||||
margin-right: auto;
|
||||
}
|
||||
h3 {
|
||||
margin: 10px 0 0 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-thingtalk-placeholders": ThingTalkPlaceholders;
|
||||
}
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { AutomationConfig } from "../../../../data/automation";
|
||||
|
||||
export interface ThingtalkDialogParams {
|
||||
callback: (config: Partial<AutomationConfig> | undefined) => void;
|
||||
input?: string;
|
||||
}
|
||||
|
||||
export const loadThingtalkDialog = () => import("./dialog-thingtalk");
|
||||
|
||||
export const showThingtalkDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: ThingtalkDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "ha-dialog-thinktalk",
|
||||
dialogImport: loadThingtalkDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -3,39 +3,41 @@ import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiContentPaste,
|
||||
mdiDrag,
|
||||
mdiPlus,
|
||||
mdiContentPaste,
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { SortableEvent } from "sortablejs";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import type { HaSelect } from "../../../../components/ha-select";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { Trigger, AutomationClipboard } from "../../../../data/automation";
|
||||
import { AutomationClipboard, Trigger } from "../../../../data/automation";
|
||||
import { TRIGGER_TYPES } from "../../../../data/trigger";
|
||||
import { sortableStyles } from "../../../../resources/ha-sortable-style";
|
||||
import { SortableInstance } from "../../../../resources/sortable";
|
||||
import { loadSortable } from "../../../../resources/sortable.ondemand";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { Entries, HomeAssistant } from "../../../../types";
|
||||
import "./ha-automation-trigger-row";
|
||||
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
|
||||
import "./types/ha-automation-trigger-calendar";
|
||||
import "./types/ha-automation-trigger-conversation";
|
||||
import "./types/ha-automation-trigger-device";
|
||||
import "./types/ha-automation-trigger-event";
|
||||
import "./types/ha-automation-trigger-geo_location";
|
||||
@@ -43,7 +45,6 @@ import "./types/ha-automation-trigger-homeassistant";
|
||||
import "./types/ha-automation-trigger-mqtt";
|
||||
import "./types/ha-automation-trigger-numeric_state";
|
||||
import "./types/ha-automation-trigger-persistent_notification";
|
||||
import "./types/ha-automation-trigger-conversation";
|
||||
import "./types/ha-automation-trigger-state";
|
||||
import "./types/ha-automation-trigger-sun";
|
||||
import "./types/ha-automation-trigger-tag";
|
||||
@@ -52,7 +53,6 @@ import "./types/ha-automation-trigger-time";
|
||||
import "./types/ha-automation-trigger-time_pattern";
|
||||
import "./types/ha-automation-trigger-webhook";
|
||||
import "./types/ha-automation-trigger-zone";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
|
||||
const PASTE_VALUE = "__paste__";
|
||||
|
||||
@@ -339,7 +339,7 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
|
||||
private _processedTypes = memoizeOne(
|
||||
(localize: LocalizeFunc): [string, string, string][] =>
|
||||
Object.entries(TRIGGER_TYPES)
|
||||
(Object.entries(TRIGGER_TYPES) as Entries<typeof TRIGGER_TYPES>)
|
||||
.map(
|
||||
([action, icon]) =>
|
||||
[
|
||||
|
@@ -123,10 +123,17 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.calendar.${schema.name}`
|
||||
);
|
||||
): string => {
|
||||
switch (schema.name) {
|
||||
case "entity_id":
|
||||
return this.hass.localize("ui.components.entity.entity-picker.entity");
|
||||
case "event":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.calendar.event"
|
||||
);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { mdiDelete, mdiDownload, mdiPlus } from "@mdi/js";
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import { mdiDelete, mdiDownload, mdiPlus } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoize from "memoize-one";
|
||||
@@ -48,15 +48,15 @@ class HaConfigBackup extends LitElement {
|
||||
@state() private _backupData?: BackupData;
|
||||
|
||||
private _columns = memoize(
|
||||
(narrow, _language): DataTableColumnContainer => ({
|
||||
(narrow, _language): DataTableColumnContainer<BackupContent> => ({
|
||||
name: {
|
||||
title: this.hass.localize("ui.panel.config.backup.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
template: (entry: string, backup: BackupContent) =>
|
||||
html`${entry}
|
||||
template: (backup) =>
|
||||
html`${backup.name}
|
||||
<div class="secondary">${backup.path}</div>`,
|
||||
},
|
||||
size: {
|
||||
@@ -65,7 +65,7 @@ class HaConfigBackup extends LitElement {
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (entry: number) => Math.ceil(entry * 10) / 10 + " MB",
|
||||
template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
|
||||
},
|
||||
date: {
|
||||
title: this.hass.localize("ui.panel.config.backup.created"),
|
||||
@@ -74,15 +74,15 @@ class HaConfigBackup extends LitElement {
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (entry: string) =>
|
||||
relativeTime(new Date(entry), this.hass.locale),
|
||||
template: (backup) =>
|
||||
relativeTime(new Date(backup.date), this.hass.locale),
|
||||
},
|
||||
|
||||
actions: {
|
||||
title: "",
|
||||
width: "15%",
|
||||
type: "overflow-menu",
|
||||
template: (_: string, backup: BackupContent) =>
|
||||
template: (backup) =>
|
||||
html`<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
|
@@ -10,14 +10,14 @@ import {
|
||||
} from "@mdi/js";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
html,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
@@ -32,7 +32,6 @@ import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { showAutomationEditor } from "../../../data/automation";
|
||||
import {
|
||||
BlueprintDomain,
|
||||
BlueprintMetaData,
|
||||
Blueprints,
|
||||
deleteBlueprint,
|
||||
@@ -50,10 +49,12 @@ import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showAddBlueprintDialog } from "./show-dialog-import-blueprint";
|
||||
|
||||
interface BlueprintMetaDataPath extends BlueprintMetaData {
|
||||
type BlueprintMetaDataPath = BlueprintMetaData & {
|
||||
path: string;
|
||||
error: boolean;
|
||||
}
|
||||
type: "automation" | "script";
|
||||
fullpath: string;
|
||||
};
|
||||
|
||||
const createNewFunctions = {
|
||||
automation: (blueprintMeta: BlueprintMetaDataPath) => {
|
||||
@@ -86,7 +87,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
>;
|
||||
|
||||
private _processedBlueprints = memoizeOne(
|
||||
(blueprints: Record<string, Blueprints>) => {
|
||||
(blueprints: Record<string, Blueprints>): BlueprintMetaDataPath[] => {
|
||||
const result: any[] = [];
|
||||
Object.entries(blueprints).forEach(([type, typeBlueprints]) =>
|
||||
Object.entries(typeBlueprints).forEach(([path, blueprint]) => {
|
||||
@@ -125,9 +126,9 @@ class HaBlueprintOverview extends LitElement {
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: narrow
|
||||
? (name, entity: any) => html`
|
||||
${name}<br />
|
||||
<div class="secondary">${entity.path}</div>
|
||||
? (blueprint) => html`
|
||||
${blueprint.name}<br />
|
||||
<div class="secondary">${blueprint.path}</div>
|
||||
`
|
||||
: undefined,
|
||||
},
|
||||
@@ -135,9 +136,9 @@ class HaBlueprintOverview extends LitElement {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.blueprint.overview.headers.type"
|
||||
),
|
||||
template: (type: BlueprintDomain) =>
|
||||
template: (blueprint) =>
|
||||
html`${this.hass.localize(
|
||||
`ui.panel.config.blueprint.overview.types.${type}`
|
||||
`ui.panel.config.blueprint.overview.types.${blueprint.type}`
|
||||
)}`,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
@@ -163,7 +164,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
title: "",
|
||||
width: this.narrow ? undefined : "10%",
|
||||
type: "overflow-menu",
|
||||
template: (_: string, blueprint) =>
|
||||
template: (blueprint) =>
|
||||
blueprint.error
|
||||
? html`<ha-svg-icon
|
||||
style="color: var(--error-color); display: block; margin-inline-end: 12px; margin-inline-start: auto;"
|
||||
@@ -177,7 +178,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
{
|
||||
path: mdiPlus,
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.blueprint.overview.create_${blueprint.domain}`
|
||||
`ui.panel.config.blueprint.overview.create_${blueprint.type}`
|
||||
),
|
||||
action: () => this._createNew(blueprint),
|
||||
},
|
||||
@@ -324,7 +325,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const blueprint = this._processedBlueprints(this.blueprints).find(
|
||||
(b) => b.fullpath === ev.detail.id
|
||||
);
|
||||
)!;
|
||||
if (blueprint.error) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.blueprint.overview.error", {
|
||||
|
@@ -62,6 +62,16 @@ class DialogCloudCertificate extends LitElement {
|
||||
)}
|
||||
${certificateInfo.fingerprint}
|
||||
</p>
|
||||
<p class="break-word">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.cloud.dialog_certificate.alternative_names"
|
||||
)}
|
||||
</p>
|
||||
<ul>
|
||||
${certificateInfo.alternative_names.map(
|
||||
(name) => html`<li><code>${name}</code></li>`
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<mwc-button @click=${this.closeDialog} slot="primaryAction">
|
||||
|
@@ -2,27 +2,26 @@ import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
||||
import { mdiCancel, mdiFilterVariant, mdiPlus } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import {
|
||||
protocolIntegrationPicked,
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
DataTableRowData,
|
||||
RowClickedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/entity/ha-battery-icon";
|
||||
@@ -33,9 +32,9 @@ import "../../../components/ha-icon-button";
|
||||
import { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries";
|
||||
import {
|
||||
computeDeviceName,
|
||||
DeviceEntityLookup,
|
||||
DeviceRegistryEntry,
|
||||
computeDeviceName,
|
||||
} from "../../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
@@ -231,7 +230,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
outputDevices = outputDevices.filter((device) => !device.disabled_by);
|
||||
}
|
||||
|
||||
outputDevices = outputDevices.map((device) => {
|
||||
const formattedOutputDevices = outputDevices.map((device) => {
|
||||
const deviceEntries = sortConfigEntries(
|
||||
device.config_entries
|
||||
.filter((entId) => entId in entryLookup)
|
||||
@@ -277,156 +276,153 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
};
|
||||
});
|
||||
|
||||
this._numHiddenDevices = startLength - outputDevices.length;
|
||||
this._numHiddenDevices = startLength - formattedOutputDevices.length;
|
||||
return {
|
||||
devicesOutput: outputDevices,
|
||||
devicesOutput: formattedOutputDevices,
|
||||
filteredConfigEntry: filterConfigEntry,
|
||||
filteredDomains,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean, showDisabled: boolean): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
title: "",
|
||||
type: "icon",
|
||||
template: (_icon, device) =>
|
||||
device.domains.length
|
||||
? html`<img
|
||||
alt=""
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl({
|
||||
domain: device.domains[0],
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
/>`
|
||||
: "",
|
||||
},
|
||||
};
|
||||
private _columns = memoizeOne((narrow: boolean, showDisabled: boolean) => {
|
||||
type DeviceItem = ReturnType<
|
||||
typeof this._devicesAndFilterDomains
|
||||
>["devicesOutput"][number];
|
||||
|
||||
if (narrow) {
|
||||
columns.name = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.device"
|
||||
),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (name, device: DataTableRowData) => html`
|
||||
${name}
|
||||
<div class="secondary">${device.area} | ${device.integration}</div>
|
||||
`,
|
||||
};
|
||||
} else {
|
||||
columns.name = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.device"
|
||||
),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
};
|
||||
}
|
||||
const columns: DataTableColumnContainer<DeviceItem> = {
|
||||
icon: {
|
||||
title: "",
|
||||
type: "icon",
|
||||
template: (device) =>
|
||||
device.domains.length
|
||||
? html`<img
|
||||
alt=""
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl({
|
||||
domain: device.domains[0],
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
/>`
|
||||
: "",
|
||||
},
|
||||
};
|
||||
|
||||
columns.manufacturer = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.manufacturer"
|
||||
),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.model = {
|
||||
title: this.hass.localize("ui.panel.config.devices.data_table.model"),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.area = {
|
||||
title: this.hass.localize("ui.panel.config.devices.data_table.area"),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.integration = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.integration"
|
||||
),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.battery_entity = {
|
||||
title: this.hass.localize("ui.panel.config.devices.data_table.battery"),
|
||||
if (narrow) {
|
||||
columns.name = {
|
||||
title: this.hass.localize("ui.panel.config.devices.data_table.device"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
type: "numeric",
|
||||
width: narrow ? "95px" : "15%",
|
||||
maxWidth: "95px",
|
||||
valueColumn: "battery_level",
|
||||
template: (batteryEntityPair: DeviceRowData["battery_entity"]) => {
|
||||
const battery =
|
||||
batteryEntityPair && batteryEntityPair[0]
|
||||
? this.hass.states[batteryEntityPair[0]]
|
||||
: undefined;
|
||||
const batteryDomain = battery
|
||||
? computeStateDomain(battery)
|
||||
: undefined;
|
||||
const batteryCharging =
|
||||
batteryEntityPair && batteryEntityPair[1]
|
||||
? this.hass.states[batteryEntityPair[1]]
|
||||
: undefined;
|
||||
|
||||
return battery &&
|
||||
(batteryDomain === "binary_sensor" || !isNaN(battery.state as any))
|
||||
? html`
|
||||
${batteryDomain === "sensor"
|
||||
? this.hass.formatEntityState(battery)
|
||||
: nothing}
|
||||
<ha-battery-icon
|
||||
.hass=${this.hass}
|
||||
.batteryStateObj=${battery}
|
||||
.batteryChargingStateObj=${batteryCharging}
|
||||
></ha-battery-icon>
|
||||
`
|
||||
: html`—`;
|
||||
},
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (device) => html`
|
||||
${device.name}
|
||||
<div class="secondary">${device.area} | ${device.integration}</div>
|
||||
`,
|
||||
};
|
||||
} else {
|
||||
columns.name = {
|
||||
title: this.hass.localize("ui.panel.config.devices.data_table.device"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
};
|
||||
if (showDisabled) {
|
||||
columns.disabled_by = {
|
||||
title: "",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.disabled_by"
|
||||
),
|
||||
type: "icon",
|
||||
template: (disabled_by) =>
|
||||
disabled_by
|
||||
? html`<div
|
||||
tabindex="0"
|
||||
style="display:inline-block; position: relative;"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiCancel}></ha-svg-icon>
|
||||
<simple-tooltip animation-delay="0" position="left">
|
||||
${this.hass.localize("ui.panel.config.devices.disabled")}
|
||||
</simple-tooltip>
|
||||
</div>`
|
||||
: "—",
|
||||
};
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
|
||||
columns.manufacturer = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.manufacturer"
|
||||
),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.model = {
|
||||
title: this.hass.localize("ui.panel.config.devices.data_table.model"),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.area = {
|
||||
title: this.hass.localize("ui.panel.config.devices.data_table.area"),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.integration = {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.integration"
|
||||
),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.battery_entity = {
|
||||
title: this.hass.localize("ui.panel.config.devices.data_table.battery"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
type: "numeric",
|
||||
width: narrow ? "95px" : "15%",
|
||||
maxWidth: "95px",
|
||||
valueColumn: "battery_level",
|
||||
template: (device) => {
|
||||
const batteryEntityPair = device.battery_entity;
|
||||
const battery =
|
||||
batteryEntityPair && batteryEntityPair[0]
|
||||
? this.hass.states[batteryEntityPair[0]]
|
||||
: undefined;
|
||||
const batteryDomain = battery ? computeStateDomain(battery) : undefined;
|
||||
const batteryCharging =
|
||||
batteryEntityPair && batteryEntityPair[1]
|
||||
? this.hass.states[batteryEntityPair[1]]
|
||||
: undefined;
|
||||
|
||||
return battery &&
|
||||
(batteryDomain === "binary_sensor" || !isNaN(battery.state as any))
|
||||
? html`
|
||||
${batteryDomain === "sensor"
|
||||
? this.hass.formatEntityState(battery)
|
||||
: nothing}
|
||||
<ha-battery-icon
|
||||
.hass=${this.hass}
|
||||
.batteryStateObj=${battery}
|
||||
.batteryChargingStateObj=${batteryCharging}
|
||||
></ha-battery-icon>
|
||||
`
|
||||
: html`—`;
|
||||
},
|
||||
};
|
||||
if (showDisabled) {
|
||||
columns.disabled_by = {
|
||||
title: "",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.disabled_by"
|
||||
),
|
||||
type: "icon",
|
||||
template: (device) =>
|
||||
device.disabled_by
|
||||
? html`<div
|
||||
tabindex="0"
|
||||
style="display:inline-block; position: relative;"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiCancel}></ha-svg-icon>
|
||||
<simple-tooltip animation-delay="0" position="left">
|
||||
${this.hass.localize("ui.panel.config.devices.disabled")}
|
||||
</simple-tooltip>
|
||||
</div>`
|
||||
: "—",
|
||||
};
|
||||
}
|
||||
return columns;
|
||||
});
|
||||
|
||||
public willUpdate(changedProps) {
|
||||
if (changedProps.has("_searchParms")) {
|
||||
|
@@ -1336,7 +1336,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
|
||||
private async _showOptionsFlow() {
|
||||
showOptionsFlowDialog(this, this.helperConfigEntry!, null);
|
||||
showOptionsFlowDialog(this, this.helperConfigEntry!);
|
||||
}
|
||||
|
||||
private _switchAsDomainsSorted = memoizeOne(
|
||||
|
@@ -183,7 +183,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.entities.picker.headers.state_icon"
|
||||
),
|
||||
type: "icon",
|
||||
template: (_, entry: EntityRow) => html`
|
||||
template: (entry) => html`
|
||||
<ha-state-icon
|
||||
title=${ifDefined(entry.entity?.state)}
|
||||
slot="item-icon"
|
||||
@@ -201,12 +201,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: narrow
|
||||
? (name, entity: EntityRow) => html`
|
||||
${name}<br />
|
||||
? (entry) => html`
|
||||
${entry.name}<br />
|
||||
<div class="secondary">
|
||||
${entity.entity_id} |
|
||||
${this.hass.localize(`component.${entity.platform}.title`) ||
|
||||
entity.platform}
|
||||
${entry.entity_id} |
|
||||
${this.hass.localize(`component.${entry.platform}.title`) ||
|
||||
entry.platform}
|
||||
</div>
|
||||
`
|
||||
: undefined,
|
||||
@@ -228,8 +228,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "20%",
|
||||
template: (platform) =>
|
||||
this.hass.localize(`component.${platform}.title`) || platform,
|
||||
template: (entry) =>
|
||||
this.hass.localize(`component.${entry.platform}.title`) ||
|
||||
entry.platform,
|
||||
},
|
||||
area: {
|
||||
title: this.hass.localize(
|
||||
@@ -248,10 +249,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
hidden: narrow || !showDisabled,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
template: (disabled_by: EntityRegistryEntry["disabled_by"]) =>
|
||||
disabled_by === null
|
||||
template: (entry) =>
|
||||
entry.disabled_by === null
|
||||
? "—"
|
||||
: this.hass.localize(`config_entry.disabled_by.${disabled_by}`),
|
||||
: this.hass.localize(
|
||||
`config_entry.disabled_by.${entry.disabled_by}`
|
||||
),
|
||||
},
|
||||
status: {
|
||||
title: this.hass.localize(
|
||||
@@ -261,11 +264,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "68px",
|
||||
template: (_status, entity: EntityRow) =>
|
||||
entity.unavailable ||
|
||||
entity.disabled_by ||
|
||||
entity.hidden_by ||
|
||||
entity.readonly
|
||||
template: (entry) =>
|
||||
entry.unavailable ||
|
||||
entry.disabled_by ||
|
||||
entry.hidden_by ||
|
||||
entry.readonly
|
||||
? html`
|
||||
<div
|
||||
tabindex="0"
|
||||
@@ -273,32 +276,32 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
>
|
||||
<ha-svg-icon
|
||||
style=${styleMap({
|
||||
color: entity.unavailable ? "var(--error-color)" : "",
|
||||
color: entry.unavailable ? "var(--error-color)" : "",
|
||||
})}
|
||||
.path=${entity.restored
|
||||
.path=${entry.restored
|
||||
? mdiRestoreAlert
|
||||
: entity.unavailable
|
||||
: entry.unavailable
|
||||
? mdiAlertCircle
|
||||
: entity.disabled_by
|
||||
: entry.disabled_by
|
||||
? mdiCancel
|
||||
: entity.hidden_by
|
||||
: entry.hidden_by
|
||||
? mdiEyeOff
|
||||
: mdiPencilOff}
|
||||
></ha-svg-icon>
|
||||
<simple-tooltip animation-delay="0" position="left">
|
||||
${entity.restored
|
||||
${entry.restored
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.restored"
|
||||
)
|
||||
: entity.unavailable
|
||||
: entry.unavailable
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.unavailable"
|
||||
)
|
||||
: entity.disabled_by
|
||||
: entry.disabled_by
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.disabled"
|
||||
)
|
||||
: entity.hidden_by
|
||||
: entry.hidden_by
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.entities.picker.status.hidden"
|
||||
)
|
||||
|
@@ -340,6 +340,7 @@ class HaScheduleForm extends LitElement {
|
||||
});
|
||||
|
||||
if (!isSameDay(start, end)) {
|
||||
this.requestUpdate(`_${day}`);
|
||||
info.revert();
|
||||
}
|
||||
}
|
||||
@@ -374,6 +375,7 @@ class HaScheduleForm extends LitElement {
|
||||
});
|
||||
|
||||
if (!isSameDay(start, end)) {
|
||||
this.requestUpdate(`_${day}`);
|
||||
info.revert();
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,15 @@
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import { mdiAlertCircle, mdiPencilOff, mdiPlus } from "@mdi/js";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../../common/translations/localize";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
@@ -16,7 +19,10 @@ import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
|
||||
import {
|
||||
ConfigEntry,
|
||||
subscribeConfigEntries,
|
||||
} from "../../../data/config_entries";
|
||||
import { getConfigFlowHandlers } from "../../../data/config_flow";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
@@ -24,6 +30,7 @@ import {
|
||||
} from "../../../data/entity_registry";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -35,9 +42,19 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "../integrations/ha-integration-overflow-menu";
|
||||
import { HelperDomain, isHelperDomain } from "./const";
|
||||
import { isHelperDomain } from "./const";
|
||||
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
|
||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
|
||||
type HelperItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
entity_id: string;
|
||||
editable?: boolean;
|
||||
type: string;
|
||||
configEntry?: ConfigEntry;
|
||||
entity?: HassEntity;
|
||||
};
|
||||
|
||||
// This groups items by a key but only returns last entry per key.
|
||||
const groupByOne = <T>(
|
||||
@@ -76,18 +93,45 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _configEntries?: Record<string, ConfigEntry>;
|
||||
|
||||
public hassSubscribe() {
|
||||
return [
|
||||
subscribeConfigEntries(
|
||||
this.hass,
|
||||
async (messages) => {
|
||||
const newEntries = this._configEntries
|
||||
? { ...this._configEntries }
|
||||
: {};
|
||||
messages.forEach((message) => {
|
||||
if (message.type === null || message.type === "added") {
|
||||
newEntries[message.entry.entry_id] = message.entry;
|
||||
} else if (message.type === "removed") {
|
||||
delete newEntries[message.entry.entry_id];
|
||||
} else if (message.type === "updated") {
|
||||
newEntries[message.entry.entry_id] = message.entry;
|
||||
}
|
||||
});
|
||||
this._configEntries = newEntries;
|
||||
},
|
||||
{ type: ["helper"] }
|
||||
),
|
||||
subscribeEntityRegistry(this.hass.connection!, (entries) => {
|
||||
this._entityEntries = groupByOne(entries, (entry) => entry.entity_id);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
const columns: DataTableColumnContainer<HelperItem> = {
|
||||
icon: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.helpers.picker.headers.icon"),
|
||||
type: "icon",
|
||||
template: (icon, helper: any) =>
|
||||
template: (helper) =>
|
||||
helper.entity
|
||||
? html`<ha-state-icon .state=${helper.entity}></ha-state-icon>`
|
||||
: html`<ha-svg-icon
|
||||
.path=${icon}
|
||||
.path=${helper.icon}
|
||||
style="color: var(--error-color)"
|
||||
></ha-svg-icon>`,
|
||||
},
|
||||
@@ -98,10 +142,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
template: (name, item: any) => html`
|
||||
${name}
|
||||
template: (helper) => html`
|
||||
${helper.name}
|
||||
${narrow
|
||||
? html`<div class="secondary">${item.entity_id}</div> `
|
||||
? html`<div class="secondary">${helper.entity_id}</div> `
|
||||
: ""}
|
||||
`,
|
||||
},
|
||||
@@ -119,11 +163,13 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
sortable: true,
|
||||
width: "25%",
|
||||
filterable: true,
|
||||
template: (type: HelperDomain, row) =>
|
||||
row.configEntry
|
||||
? domainToName(localize, type)
|
||||
template: (helper) =>
|
||||
helper.configEntry
|
||||
? domainToName(localize, helper.type)
|
||||
: html`
|
||||
${localize(`ui.panel.config.helpers.types.${type}`) || type}
|
||||
${localize(
|
||||
`ui.panel.config.helpers.types.${helper.type}` as LocalizeKeys
|
||||
) || helper.type}
|
||||
`,
|
||||
};
|
||||
columns.editable = {
|
||||
@@ -132,8 +178,8 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.helpers.picker.headers.editable"
|
||||
),
|
||||
type: "icon",
|
||||
template: (editable) => html`
|
||||
${!editable
|
||||
template: (helper) => html`
|
||||
${!helper.editable
|
||||
? html`
|
||||
<div
|
||||
tabindex="0"
|
||||
@@ -159,7 +205,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
stateItems: HassEntity[],
|
||||
entityEntries: Record<string, EntityRegistryEntry>,
|
||||
configEntries: Record<string, ConfigEntry>
|
||||
) => {
|
||||
): HelperItem[] => {
|
||||
const configEntriesCopy = { ...configEntries };
|
||||
|
||||
const states = stateItems.map((entityState) => {
|
||||
@@ -256,7 +302,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._getConfigEntries();
|
||||
if (this.route.path === "/add") {
|
||||
this._handleAdd();
|
||||
}
|
||||
@@ -313,9 +358,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
showConfigFlowDialog(this, {
|
||||
dialogClosedCallback: () => {
|
||||
this._getConfigEntries();
|
||||
},
|
||||
startFlowHandler: domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
});
|
||||
@@ -366,21 +408,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entries) => {
|
||||
this._entityEntries = groupByOne(entries, (entry) => entry.entity_id);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private async _getConfigEntries() {
|
||||
this._configEntries = groupByOne(
|
||||
await getConfigEntries(this.hass, { type: ["helper"] }),
|
||||
(entry) => entry.entry_id
|
||||
);
|
||||
}
|
||||
|
||||
private async _openEditDialog(ev: CustomEvent): Promise<void> {
|
||||
const id = (ev.detail as RowClickedEvent).id;
|
||||
if (id.includes(".")) {
|
||||
@@ -391,12 +418,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _createHelpler() {
|
||||
showHelperDetailDialog(this, {
|
||||
dialogClosedCallback: (params) => {
|
||||
if (params.flowFinished) {
|
||||
this._getConfigEntries();
|
||||
}
|
||||
},
|
||||
});
|
||||
showHelperDetailDialog(this, {});
|
||||
}
|
||||
}
|
||||
|
@@ -27,12 +27,7 @@ import { documentationUrl } from "../../../util/documentation-url";
|
||||
const JS_TYPE = __BUILD__;
|
||||
const JS_VERSION = __VERSION__;
|
||||
|
||||
const PAGES: Array<{
|
||||
name: string;
|
||||
path: string;
|
||||
iconPath: string;
|
||||
iconColor: string;
|
||||
}> = [
|
||||
const PAGES = [
|
||||
{
|
||||
name: "change_log",
|
||||
path: "/latest-release-notes/",
|
||||
@@ -75,7 +70,12 @@ const PAGES: Array<{
|
||||
iconPath: mdiFileDocument,
|
||||
iconColor: "#518C43",
|
||||
},
|
||||
];
|
||||
] as const satisfies readonly {
|
||||
name: string;
|
||||
path: string;
|
||||
iconPath: string;
|
||||
iconColor: string;
|
||||
}[];
|
||||
|
||||
class HaConfigInfo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
@@ -1024,7 +1024,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
showOptionsFlowDialog(
|
||||
this,
|
||||
ev.target.closest(".config_entry").configEntry,
|
||||
this._manifest
|
||||
{ manifest: this._manifest }
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -38,7 +38,7 @@ export class ZHAClustersDataTable extends LitElement {
|
||||
});
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer =>
|
||||
(narrow: boolean): DataTableColumnContainer<ClusterRowData> =>
|
||||
narrow
|
||||
? {
|
||||
name: {
|
||||
@@ -57,7 +57,7 @@ export class ZHAClustersDataTable extends LitElement {
|
||||
},
|
||||
id: {
|
||||
title: "ID",
|
||||
template: (id: number) => html` ${formatAsPaddedHex(id)} `,
|
||||
template: (cluster) => html` ${formatAsPaddedHex(cluster.id)} `,
|
||||
sortable: true,
|
||||
width: "25%",
|
||||
},
|
||||
|
@@ -67,9 +67,9 @@ export class ZHADeviceEndpointDataTable extends LitElement {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (name, device: any) => html`
|
||||
template: (device) => html`
|
||||
<a href=${`/config/devices/device/${device.dev_id}`}>
|
||||
${name}
|
||||
${device.name}
|
||||
</a>
|
||||
`,
|
||||
},
|
||||
@@ -86,9 +86,9 @@ export class ZHADeviceEndpointDataTable extends LitElement {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (name, device: any) => html`
|
||||
template: (device) => html`
|
||||
<a href=${`/config/devices/device/${device.dev_id}`}>
|
||||
${name}
|
||||
${device.name}
|
||||
</a>
|
||||
`,
|
||||
},
|
||||
@@ -102,10 +102,10 @@ export class ZHADeviceEndpointDataTable extends LitElement {
|
||||
sortable: false,
|
||||
filterable: false,
|
||||
width: "50%",
|
||||
template: (entities) => html`
|
||||
${entities.length
|
||||
? entities.length > 3
|
||||
? html`${entities
|
||||
template: (device) => html`
|
||||
${device.entities.length
|
||||
? device.entities.length > 3
|
||||
? html`${device.entities
|
||||
.slice(0, 2)
|
||||
.map(
|
||||
(entity) =>
|
||||
@@ -115,8 +115,8 @@ export class ZHADeviceEndpointDataTable extends LitElement {
|
||||
${entity.name || entity.original_name}
|
||||
</div>`
|
||||
)}
|
||||
<div>And ${entities.length - 2} more...</div>`
|
||||
: entities.map(
|
||||
<div>And ${device.entities.length - 2} more...</div>`
|
||||
: device.entities.map(
|
||||
(entity) =>
|
||||
html`<div
|
||||
style="overflow: hidden; text-overflow: ellipsis;"
|
||||
|
@@ -18,7 +18,7 @@ import {
|
||||
} from "../../../../../components/data-table/ha-data-table";
|
||||
import "../../../../../components/ha-fab";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import { fetchGroups, ZHADevice, ZHAGroup } from "../../../../../data/zha";
|
||||
import { fetchGroups, ZHAGroup } from "../../../../../data/zha";
|
||||
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../../../types";
|
||||
@@ -71,7 +71,7 @@ export class ZHAGroupsDashboard extends LitElement {
|
||||
});
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer =>
|
||||
(narrow: boolean): DataTableColumnContainer<GroupRowData> =>
|
||||
narrow
|
||||
? {
|
||||
name: {
|
||||
@@ -94,16 +94,14 @@ export class ZHAGroupsDashboard extends LitElement {
|
||||
title: this.hass.localize("ui.panel.config.zha.groups.group_id"),
|
||||
type: "numeric",
|
||||
width: "15%",
|
||||
template: (groupId: number) => html`
|
||||
${formatAsPaddedHex(groupId)}
|
||||
`,
|
||||
template: (group) => html` ${formatAsPaddedHex(group.group_id)} `,
|
||||
sortable: true,
|
||||
},
|
||||
members: {
|
||||
title: this.hass.localize("ui.panel.config.zha.groups.members"),
|
||||
type: "numeric",
|
||||
width: "15%",
|
||||
template: (members: ZHADevice[]) => html` ${members.length} `,
|
||||
template: (group) => html` ${group.members.length} `,
|
||||
sortable: true,
|
||||
},
|
||||
}
|
||||
|
@@ -41,15 +41,15 @@ class ZWaveJSProvisioned extends LitElement {
|
||||
}
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer => ({
|
||||
(narrow: boolean): DataTableColumnContainer<ZwaveJSProvisioningEntry> => ({
|
||||
included: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.zwave_js.provisioned.included"
|
||||
),
|
||||
type: "icon",
|
||||
width: "100px",
|
||||
template: (_info, provisioningEntry: any) =>
|
||||
provisioningEntry.additional_properties.nodeId
|
||||
template: (entry) =>
|
||||
entry.additional_properties.nodeId
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.label=${this.hass.localize(
|
||||
@@ -81,14 +81,16 @@ class ZWaveJSProvisioned extends LitElement {
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (securityClasses: SecurityClass[]) =>
|
||||
securityClasses
|
||||
template: (entry) => {
|
||||
const securityClasses = entry.security_classes;
|
||||
return securityClasses
|
||||
.map((secClass) =>
|
||||
this.hass.localize(
|
||||
`ui.panel.config.zwave_js.security_classes.${SecurityClass[secClass]}.title`
|
||||
)
|
||||
)
|
||||
.join(", "),
|
||||
.join(", ");
|
||||
},
|
||||
},
|
||||
unprovision: {
|
||||
title: this.hass.localize(
|
||||
@@ -96,13 +98,13 @@ class ZWaveJSProvisioned extends LitElement {
|
||||
),
|
||||
type: "icon-button",
|
||||
width: "100px",
|
||||
template: (_info, provisioningEntry: any) => html`
|
||||
template: (entry) => html`
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.provisioned.unprovison"
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
.provisioningEntry=${provisioningEntry}
|
||||
.provisioningEntry=${entry}
|
||||
@click=${this._unprovision}
|
||||
></ha-icon-button>
|
||||
`,
|
||||
|
@@ -38,6 +38,15 @@ import { HomeAssistant, Route } from "../../../../types";
|
||||
import { lovelaceTabs } from "../ha-config-lovelace";
|
||||
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
|
||||
|
||||
type DataTableItem = Pick<
|
||||
LovelaceDashboard,
|
||||
"icon" | "title" | "show_in_sidebar" | "require_admin" | "mode" | "url_path"
|
||||
> & {
|
||||
default: boolean;
|
||||
filename: string;
|
||||
iconColor?: string;
|
||||
};
|
||||
|
||||
@customElement("ha-config-lovelace-dashboards")
|
||||
export class HaConfigLovelaceDashboards extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -52,19 +61,19 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
|
||||
private _columns = memoize(
|
||||
(narrow: boolean, _language, dashboards): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
const columns: DataTableColumnContainer<DataTableItem> = {
|
||||
icon: {
|
||||
title: "",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.icon"
|
||||
),
|
||||
type: "icon",
|
||||
template: (icon, dashboard) =>
|
||||
icon
|
||||
template: (dashboard) =>
|
||||
dashboard.icon
|
||||
? html`
|
||||
<ha-icon
|
||||
slot="item-icon"
|
||||
.icon=${icon}
|
||||
.icon=${dashboard.icon}
|
||||
style=${ifDefined(
|
||||
dashboard.iconColor
|
||||
? `color: ${dashboard.iconColor}`
|
||||
@@ -82,9 +91,9 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
template: (title, dashboard: any) => {
|
||||
template: (dashboard) => {
|
||||
const titleTemplate = html`
|
||||
${title}
|
||||
${dashboard.title}
|
||||
${dashboard.default
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
@@ -123,10 +132,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "20%",
|
||||
template: (mode) => html`
|
||||
template: (dashboard) => html`
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.conf_mode.${mode}`
|
||||
) || mode}
|
||||
`ui.panel.config.lovelace.dashboards.conf_mode.${dashboard.mode}`
|
||||
) || dashboard.mode}
|
||||
`,
|
||||
};
|
||||
if (dashboards.some((dashboard) => dashboard.filename)) {
|
||||
@@ -146,8 +155,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
sortable: true,
|
||||
type: "icon",
|
||||
width: "100px",
|
||||
template: (requireAdmin: boolean) =>
|
||||
requireAdmin
|
||||
template: (dashboard) =>
|
||||
dashboard.require_admin
|
||||
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: html`—`,
|
||||
};
|
||||
@@ -157,8 +166,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
),
|
||||
type: "icon",
|
||||
width: "121px",
|
||||
template: (sidebar) =>
|
||||
sidebar
|
||||
template: (dashboard) =>
|
||||
dashboard.show_in_sidebar
|
||||
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: html`—`,
|
||||
};
|
||||
@@ -171,12 +180,12 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
),
|
||||
filterable: true,
|
||||
width: "100px",
|
||||
template: (urlPath) =>
|
||||
template: (dashboard) =>
|
||||
narrow
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiOpenInNew}
|
||||
.urlPath=${urlPath}
|
||||
.urlPath=${dashboard.url_path}
|
||||
@click=${this._navigate}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.open"
|
||||
@@ -184,7 +193,9 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
></ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
<mwc-button .urlPath=${urlPath} @click=${this._navigate}
|
||||
<mwc-button
|
||||
.urlPath=${dashboard.url_path}
|
||||
@click=${this._navigate}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.open"
|
||||
)}</mwc-button
|
||||
@@ -202,7 +213,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
).mode;
|
||||
const defaultUrlPath = this.hass.defaultPanel;
|
||||
const isDefault = defaultUrlPath === "lovelace";
|
||||
const result: Record<string, any>[] = [
|
||||
const result: DataTableItem[] = [
|
||||
{
|
||||
icon: "hass:view-dashboard",
|
||||
title: this.hass.localize("panel.states"),
|
||||
@@ -224,6 +235,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
url_path: "energy",
|
||||
filename: "",
|
||||
iconColor: "var(--label-badge-yellow)",
|
||||
default: false,
|
||||
require_admin: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -40,7 +40,7 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
@state() private _resources: LovelaceResource[] = [];
|
||||
|
||||
private _columns = memoize(
|
||||
(_language): DataTableColumnContainer => ({
|
||||
(_language): DataTableColumnContainer<LovelaceResource> => ({
|
||||
url: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.lovelace.resources.picker.headers.url"
|
||||
@@ -58,10 +58,10 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "30%",
|
||||
template: (type) => html`
|
||||
template: (resource) => html`
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.resources.types.${type}`
|
||||
) || type}
|
||||
`ui.panel.config.lovelace.resources.types.${resource.type}`
|
||||
) || resource.type}
|
||||
`,
|
||||
},
|
||||
})
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-tab";
|
||||
import "@material/mwc-tab-bar";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-alert";
|
||||
|
@@ -47,6 +47,10 @@ import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
|
||||
type SceneItem = SceneEntity & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
@customElement("ha-scene-dashboard")
|
||||
class HaSceneDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -66,7 +70,7 @@ class HaSceneDashboard extends LitElement {
|
||||
@state() private _filterValue?;
|
||||
|
||||
private _scenes = memoizeOne(
|
||||
(scenes: SceneEntity[], filteredScenes?: string[] | null) => {
|
||||
(scenes: SceneEntity[], filteredScenes?: string[] | null): SceneItem[] => {
|
||||
if (filteredScenes === null) {
|
||||
return [];
|
||||
}
|
||||
@@ -83,14 +87,14 @@ class HaSceneDashboard extends LitElement {
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(_language, narrow): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
const columns: DataTableColumnContainer<SceneItem> = {
|
||||
icon: {
|
||||
title: "",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.scene.picker.headers.state"
|
||||
),
|
||||
type: "icon",
|
||||
template: (_, scene) => html`
|
||||
template: (scene) => html`
|
||||
<ha-state-icon .state=${scene}></ha-state-icon>
|
||||
`,
|
||||
},
|
||||
@@ -112,20 +116,18 @@ class HaSceneDashboard extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
width: "30%",
|
||||
template: (last_activated) => {
|
||||
const date = new Date(last_activated);
|
||||
template: (scene) => {
|
||||
const lastActivated = scene.state;
|
||||
if (!lastActivated || isUnavailableState(lastActivated)) {
|
||||
return this.hass.localize("ui.components.relative_time.never");
|
||||
}
|
||||
const date = new Date(scene.state);
|
||||
const now = new Date();
|
||||
const dayDifference = differenceInDays(now, date);
|
||||
return html`
|
||||
${last_activated && !isUnavailableState(last_activated)
|
||||
? dayDifference > 3
|
||||
? formatShortDateTime(
|
||||
date,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)
|
||||
: relativeTime(date, this.hass.locale)
|
||||
: this.hass.localize("ui.components.relative_time.never")}
|
||||
${dayDifference > 3
|
||||
? formatShortDateTime(date, this.hass.locale, this.hass.config)
|
||||
: relativeTime(date, this.hass.locale)}
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -133,7 +135,7 @@ class HaSceneDashboard extends LitElement {
|
||||
columns.only_editable = {
|
||||
title: "",
|
||||
width: "56px",
|
||||
template: (_info, scene: any) =>
|
||||
template: (scene) =>
|
||||
!scene.attributes.id
|
||||
? html`
|
||||
<simple-tooltip animation-delay="0" position="left">
|
||||
@@ -152,7 +154,7 @@ class HaSceneDashboard extends LitElement {
|
||||
title: "",
|
||||
width: "72px",
|
||||
type: "overflow-menu",
|
||||
template: (_: string, scene: any) => html`
|
||||
template: (scene) => html`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
narrow
|
||||
|
@@ -443,9 +443,6 @@ export class HaSceneEditor extends SubscribeMixin(
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.scene.editor.entities.device_entities"
|
||||
)}
|
||||
<ha-entity-picker
|
||||
@value-changed=${this._entityPicked}
|
||||
.excludeDomains=${SCENE_IGNORED_DOMAINS}
|
||||
|
@@ -55,7 +55,7 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import type { Entries, HomeAssistant, Route } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "./blueprint-script-editor";
|
||||
@@ -529,7 +529,9 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
const validation = await validateConfig(this.hass, {
|
||||
action: this._config.sequence,
|
||||
});
|
||||
this._validationErrors = Object.entries(validation).map(([key, value]) =>
|
||||
this._validationErrors = (
|
||||
Object.entries(validation) as Entries<typeof validation>
|
||||
).map(([key, value]) =>
|
||||
value.valid
|
||||
? ""
|
||||
: html`${this.hass.localize(
|
||||
|
@@ -7,16 +7,15 @@ import {
|
||||
mdiPlus,
|
||||
mdiTransitConnection,
|
||||
} from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { differenceInDays } from "date-fns/esm";
|
||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
@@ -29,13 +28,18 @@ import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import {
|
||||
ScriptEntity,
|
||||
deleteScript,
|
||||
fetchScriptFileConfig,
|
||||
getScriptStateConfig,
|
||||
showScriptEditor,
|
||||
triggerScript,
|
||||
} from "../../../data/script";
|
||||
import { findRelated } from "../../../data/search";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -45,18 +49,18 @@ import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showNewAutomationDialog } from "../automation/show-dialog-new-automation";
|
||||
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import { findRelated } from "../../../data/search";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
|
||||
type ScriptItem = ScriptEntity & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
@customElement("ha-script-picker")
|
||||
class HaScriptPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public scripts!: HassEntity[];
|
||||
@property() public scripts!: ScriptEntity[];
|
||||
|
||||
@property() public isWide!: boolean;
|
||||
|
||||
@@ -75,7 +79,10 @@ class HaScriptPicker extends LitElement {
|
||||
@state() private _filterValue?;
|
||||
|
||||
private _scripts = memoizeOne(
|
||||
(scripts: HassEntity[], filteredScripts?: string[] | null) => {
|
||||
(
|
||||
scripts: ScriptEntity[],
|
||||
filteredScripts?: string[] | null
|
||||
): ScriptItem[] => {
|
||||
if (filteredScripts === null) {
|
||||
return [];
|
||||
}
|
||||
@@ -93,126 +100,136 @@ class HaScriptPicker extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _columns = memoizeOne((narrow, _locale): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
title: "",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.script.picker.headers.state"
|
||||
),
|
||||
type: "icon",
|
||||
template: (_icon, script) =>
|
||||
html`<ha-state-icon
|
||||
.state=${script}
|
||||
style=${styleMap({
|
||||
color:
|
||||
script.state === UNAVAILABLE ? "var(--error-color)" : "unset",
|
||||
})}
|
||||
></ha-state-icon>`,
|
||||
},
|
||||
name: {
|
||||
title: this.hass.localize("ui.panel.config.script.picker.headers.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: narrow
|
||||
? (name, script: any) => {
|
||||
const date = new Date(script.attributes.last_triggered);
|
||||
const now = new Date();
|
||||
const dayDifference = differenceInDays(now, date);
|
||||
return html`
|
||||
${name}
|
||||
<div class="secondary">
|
||||
${this.hass.localize("ui.card.automation.last_triggered")}:
|
||||
${script.attributes.last_triggered
|
||||
? dayDifference > 3
|
||||
? formatShortDateTime(
|
||||
date,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)
|
||||
: relativeTime(date, this.hass.locale)
|
||||
: this.hass.localize("ui.components.relative_time.never")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
if (!narrow) {
|
||||
columns.last_triggered = {
|
||||
sortable: true,
|
||||
width: "40%",
|
||||
title: this.hass.localize("ui.card.automation.last_triggered"),
|
||||
template: (last_triggered) => {
|
||||
const date = new Date(last_triggered);
|
||||
const now = new Date();
|
||||
const dayDifference = differenceInDays(now, date);
|
||||
return html`
|
||||
${last_triggered
|
||||
? dayDifference > 3
|
||||
? formatShortDateTime(date, this.hass.locale, this.hass.config)
|
||||
: relativeTime(date, this.hass.locale)
|
||||
: this.hass.localize("ui.components.relative_time.never")}
|
||||
`;
|
||||
private _columns = memoizeOne(
|
||||
(narrow, _locale): DataTableColumnContainer<ScriptItem> => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
title: "",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.script.picker.headers.state"
|
||||
),
|
||||
type: "icon",
|
||||
template: (script) =>
|
||||
html`<ha-state-icon
|
||||
.state=${script}
|
||||
style=${styleMap({
|
||||
color:
|
||||
script.state === UNAVAILABLE ? "var(--error-color)" : "unset",
|
||||
})}
|
||||
></ha-state-icon>`,
|
||||
},
|
||||
name: {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.script.picker.headers.name"
|
||||
),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: narrow
|
||||
? (script) => {
|
||||
const date = new Date(script.last_triggered);
|
||||
const now = new Date();
|
||||
const dayDifference = differenceInDays(now, date);
|
||||
return html`
|
||||
${script.name}
|
||||
<div class="secondary">
|
||||
${this.hass.localize("ui.card.automation.last_triggered")}:
|
||||
${script.last_triggered
|
||||
? dayDifference > 3
|
||||
? formatShortDateTime(
|
||||
date,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)
|
||||
: relativeTime(date, this.hass.locale)
|
||||
: this.hass.localize("ui.components.relative_time.never")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
if (!narrow) {
|
||||
columns.last_triggered = {
|
||||
sortable: true,
|
||||
width: "40%",
|
||||
title: this.hass.localize("ui.card.automation.last_triggered"),
|
||||
template: (script) => {
|
||||
const date = new Date(script.last_triggered);
|
||||
const now = new Date();
|
||||
const dayDifference = differenceInDays(now, date);
|
||||
return html`
|
||||
${script.last_triggered
|
||||
? dayDifference > 3
|
||||
? formatShortDateTime(
|
||||
date,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)
|
||||
: relativeTime(date, this.hass.locale)
|
||||
: this.hass.localize("ui.components.relative_time.never")}
|
||||
`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
columns.actions = {
|
||||
title: "",
|
||||
width: this.narrow ? undefined : "10%",
|
||||
type: "overflow-menu",
|
||||
template: (script) => html`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
narrow
|
||||
.items=${[
|
||||
{
|
||||
path: mdiInformationOutline,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.script.picker.show_info"
|
||||
),
|
||||
action: () => this._showInfo(script),
|
||||
},
|
||||
{
|
||||
path: mdiPlay,
|
||||
label: this.hass.localize("ui.panel.config.script.picker.run"),
|
||||
action: () => this._runScript(script),
|
||||
},
|
||||
{
|
||||
path: mdiTransitConnection,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.script.picker.show_trace"
|
||||
),
|
||||
action: () => this._showTrace(script),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
path: mdiContentDuplicate,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.script.picker.duplicate"
|
||||
),
|
||||
action: () => this._duplicate(script),
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.script.picker.delete"
|
||||
),
|
||||
path: mdiDelete,
|
||||
action: () => this._deleteConfirm(script),
|
||||
warning: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
</ha-icon-overflow-menu>
|
||||
`,
|
||||
};
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
columns.actions = {
|
||||
title: "",
|
||||
width: this.narrow ? undefined : "10%",
|
||||
type: "overflow-menu",
|
||||
template: (_: string, script: any) => html`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
narrow
|
||||
.items=${[
|
||||
{
|
||||
path: mdiInformationOutline,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.script.picker.show_info"
|
||||
),
|
||||
action: () => this._showInfo(script),
|
||||
},
|
||||
{
|
||||
path: mdiPlay,
|
||||
label: this.hass.localize("ui.panel.config.script.picker.run"),
|
||||
action: () => this._runScript(script),
|
||||
},
|
||||
{
|
||||
path: mdiTransitConnection,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.script.picker.show_trace"
|
||||
),
|
||||
action: () => this._showTrace(script),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
path: mdiContentDuplicate,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.script.picker.duplicate"
|
||||
),
|
||||
action: () => this._duplicate(script),
|
||||
},
|
||||
{
|
||||
label: this.hass.localize("ui.panel.config.script.picker.delete"),
|
||||
path: mdiDelete,
|
||||
action: () => this._deleteConfirm(script),
|
||||
warning: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
</ha-icon-overflow-menu>
|
||||
`,
|
||||
};
|
||||
|
||||
return columns;
|
||||
});
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
|
@@ -36,6 +36,7 @@ import { showTagDetailDialog } from "./show-dialog-tag-detail";
|
||||
import "./tag-image";
|
||||
|
||||
export interface TagRowData extends Tag {
|
||||
display_name: string;
|
||||
last_scanned_datetime: Date | null;
|
||||
}
|
||||
|
||||
@@ -55,94 +56,90 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
return this.hass.auth.external?.config.canWriteTag;
|
||||
}
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean, _language): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
title: "",
|
||||
label: this.hass.localize("ui.panel.config.tag.headers.icon"),
|
||||
type: "icon",
|
||||
template: (_icon, tag) => html`<tag-image .tag=${tag}></tag-image>`,
|
||||
},
|
||||
display_name: {
|
||||
title: this.hass.localize("ui.panel.config.tag.headers.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
template: (name, tag: any) =>
|
||||
html`${name}
|
||||
${narrow
|
||||
? html`<div class="secondary">
|
||||
${tag.last_scanned_datetime
|
||||
? html`<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${tag.last_scanned_datetime}
|
||||
capitalize
|
||||
></ha-relative-time>`
|
||||
: this.hass.localize("ui.panel.config.tag.never_scanned")}
|
||||
</div>`
|
||||
: ""}`,
|
||||
},
|
||||
};
|
||||
if (!narrow) {
|
||||
columns.last_scanned_datetime = {
|
||||
title: this.hass.localize("ui.panel.config.tag.headers.last_scanned"),
|
||||
sortable: true,
|
||||
direction: "desc",
|
||||
width: "20%",
|
||||
template: (last_scanned_datetime) => html`
|
||||
${last_scanned_datetime
|
||||
? html`<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${last_scanned_datetime}
|
||||
capitalize
|
||||
></ha-relative-time>`
|
||||
: this.hass.localize("ui.panel.config.tag.never_scanned")}
|
||||
`,
|
||||
};
|
||||
}
|
||||
if (this._canWriteTags) {
|
||||
columns.write = {
|
||||
title: "",
|
||||
label: this.hass.localize("ui.panel.config.tag.headers.write"),
|
||||
type: "icon-button",
|
||||
template: (_write, tag: any) =>
|
||||
html` <ha-icon-button
|
||||
.tag=${tag}
|
||||
@click=${this._handleWriteClick}
|
||||
.label=${this.hass.localize("ui.panel.config.tag.write")}
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-icon-button>`,
|
||||
};
|
||||
}
|
||||
columns.automation = {
|
||||
private _columns = memoizeOne((narrow: boolean, _language) => {
|
||||
const columns: DataTableColumnContainer<TagRowData> = {
|
||||
icon: {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_automation, tag: any) =>
|
||||
html` <ha-icon-button
|
||||
.tag=${tag}
|
||||
@click=${this._handleAutomationClick}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.tag.create_automation"
|
||||
)}
|
||||
.path=${mdiRobot}
|
||||
></ha-icon-button>`,
|
||||
label: this.hass.localize("ui.panel.config.tag.headers.icon"),
|
||||
type: "icon",
|
||||
template: (tag) => html`<tag-image .tag=${tag}></tag-image>`,
|
||||
},
|
||||
display_name: {
|
||||
title: this.hass.localize("ui.panel.config.tag.headers.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
template: (tag) =>
|
||||
html`${tag.name}
|
||||
${narrow
|
||||
? html`<div class="secondary">
|
||||
${tag.last_scanned_datetime
|
||||
? html`<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${tag.last_scanned_datetime}
|
||||
capitalize
|
||||
></ha-relative-time>`
|
||||
: this.hass.localize("ui.panel.config.tag.never_scanned")}
|
||||
</div>`
|
||||
: ""}`,
|
||||
},
|
||||
};
|
||||
if (!narrow) {
|
||||
columns.last_scanned_datetime = {
|
||||
title: this.hass.localize("ui.panel.config.tag.headers.last_scanned"),
|
||||
sortable: true,
|
||||
direction: "desc",
|
||||
width: "20%",
|
||||
template: (tag) => html`
|
||||
${tag.last_scanned_datetime
|
||||
? html`<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${tag.last_scanned_datetime}
|
||||
capitalize
|
||||
></ha-relative-time>`
|
||||
: this.hass.localize("ui.panel.config.tag.never_scanned")}
|
||||
`,
|
||||
};
|
||||
columns.edit = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (_settings, tag: any) =>
|
||||
html` <ha-icon-button
|
||||
.tag=${tag}
|
||||
@click=${this._handleEditClick}
|
||||
.label=${this.hass.localize("ui.panel.config.tag.edit")}
|
||||
.path=${mdiCog}
|
||||
></ha-icon-button>`,
|
||||
};
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
if (this._canWriteTags) {
|
||||
columns.write = {
|
||||
title: "",
|
||||
label: this.hass.localize("ui.panel.config.tag.headers.write"),
|
||||
type: "icon-button",
|
||||
template: (tag) =>
|
||||
html` <ha-icon-button
|
||||
.tag=${tag}
|
||||
@click=${this._handleWriteClick}
|
||||
.label=${this.hass.localize("ui.panel.config.tag.write")}
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-icon-button>`,
|
||||
};
|
||||
}
|
||||
columns.automation = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (tag) =>
|
||||
html` <ha-icon-button
|
||||
.tag=${tag}
|
||||
@click=${this._handleAutomationClick}
|
||||
.label=${this.hass.localize("ui.panel.config.tag.create_automation")}
|
||||
.path=${mdiRobot}
|
||||
></ha-icon-button>`,
|
||||
};
|
||||
columns.edit = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (tag) =>
|
||||
html` <ha-icon-button
|
||||
.tag=${tag}
|
||||
@click=${this._handleEditClick}
|
||||
.label=${this.hass.localize("ui.panel.config.tag.edit")}
|
||||
.path=${mdiCog}
|
||||
></ha-icon-button>`,
|
||||
};
|
||||
return columns;
|
||||
});
|
||||
|
||||
private _data = memoizeOne((tags: Tag[]): TagRowData[] =>
|
||||
tags.map((tag) => ({
|
||||
|
@@ -49,14 +49,14 @@ export class HaConfigUsers extends LitElement {
|
||||
width: "25%",
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (name, user) =>
|
||||
template: (user) =>
|
||||
narrow
|
||||
? html` ${name}<br />
|
||||
? html` ${user.name}<br />
|
||||
<div class="secondary">
|
||||
${user.username ? `${user.username} |` : ""}
|
||||
${localize(`groups.${user.group_ids[0]}`)}
|
||||
</div>`
|
||||
: html` ${name ||
|
||||
: html` ${user.name ||
|
||||
this.hass!.localize(
|
||||
"ui.panel.config.users.editor.unnamed_user"
|
||||
)}`,
|
||||
@@ -68,7 +68,7 @@ export class HaConfigUsers extends LitElement {
|
||||
width: "20%",
|
||||
direction: "asc",
|
||||
hidden: narrow,
|
||||
template: (username) => html`${username || "—"}`,
|
||||
template: (user) => html`${user.name || "—"}`,
|
||||
},
|
||||
group_ids: {
|
||||
title: localize("ui.panel.config.users.picker.headers.group"),
|
||||
@@ -77,8 +77,8 @@ export class HaConfigUsers extends LitElement {
|
||||
width: "20%",
|
||||
direction: "asc",
|
||||
hidden: narrow,
|
||||
template: (groupIds: User["group_ids"]) => html`
|
||||
${localize(`groups.${groupIds[0]}`)}
|
||||
template: (user) => html`
|
||||
${localize(`groups.${user.group_ids[0]}`)}
|
||||
`,
|
||||
},
|
||||
is_active: {
|
||||
@@ -90,8 +90,8 @@ export class HaConfigUsers extends LitElement {
|
||||
filterable: true,
|
||||
width: "80px",
|
||||
hidden: narrow,
|
||||
template: (is_active) =>
|
||||
is_active
|
||||
template: (user) =>
|
||||
user.is_active
|
||||
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: "",
|
||||
},
|
||||
@@ -104,8 +104,8 @@ export class HaConfigUsers extends LitElement {
|
||||
filterable: true,
|
||||
width: "80px",
|
||||
hidden: narrow,
|
||||
template: (generated) =>
|
||||
generated
|
||||
template: (user) =>
|
||||
user.system_generated
|
||||
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: "",
|
||||
},
|
||||
@@ -118,8 +118,10 @@ export class HaConfigUsers extends LitElement {
|
||||
filterable: true,
|
||||
width: "80px",
|
||||
hidden: narrow,
|
||||
template: (local) =>
|
||||
local ? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>` : "",
|
||||
template: (user) =>
|
||||
user.local_only
|
||||
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: "",
|
||||
},
|
||||
icons: {
|
||||
title: "",
|
||||
@@ -131,7 +133,7 @@ export class HaConfigUsers extends LitElement {
|
||||
filterable: false,
|
||||
width: "104px",
|
||||
hidden: !narrow,
|
||||
template: (_, user) => {
|
||||
template: (user) => {
|
||||
const badges = computeUserBadges(this.hass, user, false);
|
||||
return html`${badges.map(
|
||||
([icon, tooltip]) =>
|
||||
|
@@ -134,7 +134,7 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
title: "",
|
||||
type: "icon",
|
||||
hidden: narrow,
|
||||
template: (_, entry) => html`
|
||||
template: (entry) => html`
|
||||
<ha-state-icon
|
||||
title=${ifDefined(entry.entity?.state)}
|
||||
.state=${entry.entity}
|
||||
@@ -150,8 +150,8 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (name, entry) => html`
|
||||
${name}<br />
|
||||
template: (entry) => html`
|
||||
${entry.name}<br />
|
||||
<div class="secondary">${entry.entity_id}</div>
|
||||
`,
|
||||
},
|
||||
@@ -172,13 +172,13 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
filterable: true,
|
||||
width: "160px",
|
||||
type: "flex",
|
||||
template: (assistants, entry) =>
|
||||
template: (entry) =>
|
||||
html`${availableAssistants.map((key) => {
|
||||
const supported =
|
||||
!supportedEntities?.[key] ||
|
||||
supportedEntities[key].includes(entry.entity_id);
|
||||
const manual = entry.manAssistants?.includes(key);
|
||||
return assistants.includes(key)
|
||||
return entry.assistants.includes(key)
|
||||
? html`
|
||||
<voice-assistants-expose-assistant-icon
|
||||
.assistant=${key}
|
||||
@@ -199,14 +199,14 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
filterable: true,
|
||||
hidden: narrow,
|
||||
width: "15%",
|
||||
template: (aliases) =>
|
||||
aliases.length === 0
|
||||
template: (entry) =>
|
||||
entry.aliases.length === 0
|
||||
? "-"
|
||||
: aliases.length === 1
|
||||
? aliases[0]
|
||||
: entry.aliases.length === 1
|
||||
? entry.aliases[0]
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.expose.aliases",
|
||||
{ count: aliases.length }
|
||||
{ count: entry.aliases.length }
|
||||
),
|
||||
},
|
||||
remove: {
|
||||
|
@@ -19,6 +19,7 @@ import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-tip";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/search-input";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
@@ -185,6 +186,9 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
[[localize('ui.panel.developer-tools.tabs.states.description1')]]<br />
|
||||
[[localize('ui.panel.developer-tools.tabs.states.description2')]]
|
||||
</p>
|
||||
<template is="dom-if" if="[[_error]]">
|
||||
<ha-alert alert-type="error">[[_error]]</ha-alert>
|
||||
</template>
|
||||
<div class="state-wrapper flex layout horizontal">
|
||||
<div class="inputs">
|
||||
<ha-entity-picker
|
||||
@@ -355,6 +359,11 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
computed: "_computeValidJSON(parsedJSON)",
|
||||
},
|
||||
|
||||
_error: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
|
||||
_entityId: {
|
||||
type: String,
|
||||
value: "",
|
||||
@@ -490,7 +499,8 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
this.fire("hass-more-info", { entityId: ev.model.entity.entity_id });
|
||||
}
|
||||
|
||||
handleSetState() {
|
||||
async handleSetState() {
|
||||
this._error = "";
|
||||
if (!this._entityId) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
@@ -499,10 +509,14 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.hass.callApi("POST", "states/" + this._entityId, {
|
||||
state: this._state,
|
||||
attributes: this.parsedJSON,
|
||||
});
|
||||
try {
|
||||
await this.hass.callApi("POST", "states/" + this._entityId, {
|
||||
state: this._state,
|
||||
attributes: this.parsedJSON,
|
||||
});
|
||||
} catch (e) {
|
||||
this._error = e.body?.message || "Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
informationOutlineIcon() {
|
||||
|
@@ -80,7 +80,9 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(localize: LocalizeFunc): DataTableColumnContainer => ({
|
||||
(
|
||||
localize: LocalizeFunc
|
||||
): DataTableColumnContainer<DisplayedStatisticData> => ({
|
||||
displayName: {
|
||||
title: localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.data_table.name"
|
||||
@@ -123,8 +125,8 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
width: "30%",
|
||||
template: (issues_string) =>
|
||||
html`${issues_string ??
|
||||
template: (statistic) =>
|
||||
html`${statistic.issues_string ??
|
||||
localize("ui.panel.developer-tools.tabs.statistics.no_issue")}`,
|
||||
},
|
||||
fix: {
|
||||
@@ -132,9 +134,12 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
label: this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.fix"
|
||||
),
|
||||
template: (_, data: any) =>
|
||||
html`${data.issues
|
||||
? html`<mwc-button @click=${this._fixIssue} .data=${data.issues}>
|
||||
template: (statistic) =>
|
||||
html`${statistic.issues
|
||||
? html`<mwc-button
|
||||
@click=${this._fixIssue}
|
||||
.data=${statistic.issues}
|
||||
>
|
||||
${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.fix"
|
||||
)}
|
||||
@@ -146,7 +151,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
title: "",
|
||||
label: localize("ui.panel.developer-tools.tabs.statistics.adjust_sum"),
|
||||
type: "icon-button",
|
||||
template: (_info, statistic: StatisticsMetaData) =>
|
||||
template: (statistic) =>
|
||||
statistic.has_sum
|
||||
? html`
|
||||
<ha-icon-button
|
||||
|
@@ -4,6 +4,7 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-code-editor";
|
||||
import {
|
||||
@@ -44,6 +45,8 @@ class HaPanelDevTemplate extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _errorLevel?: "ERROR" | "WARNING";
|
||||
|
||||
@state() private _rendering = false;
|
||||
|
||||
@state() private _templateResult?: RenderTemplateResult;
|
||||
@@ -157,83 +160,87 @@ class HaPanelDevTemplate extends LitElement {
|
||||
size="small"
|
||||
></ha-circular-progress>`
|
||||
: ""}
|
||||
${this._error
|
||||
? html`<ha-alert
|
||||
alert-type=${this._errorLevel?.toLowerCase() || "error"}
|
||||
>${this._error}</ha-alert
|
||||
>`
|
||||
: nothing}
|
||||
${this._templateResult
|
||||
? html`${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.result_type"
|
||||
)}:
|
||||
${resultType}`
|
||||
: ""}
|
||||
<!-- prettier-ignore -->
|
||||
<pre
|
||||
class="rendered ${classMap({
|
||||
error: Boolean(this._error),
|
||||
[resultType]: resultType,
|
||||
})}"
|
||||
>${this._error}${type === "object"
|
||||
? JSON.stringify(this._templateResult!.result, null, 2)
|
||||
: this._templateResult?.result}</pre>
|
||||
${this._templateResult?.listeners.time
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.time"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
${!this._templateResult?.listeners
|
||||
? ""
|
||||
: this._templateResult.listeners.all
|
||||
? html`
|
||||
<p class="all_listeners">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.all_listeners"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: this._templateResult.listeners.domains.length ||
|
||||
this._templateResult.listeners.entities.length
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.listeners"
|
||||
)}
|
||||
</p>
|
||||
<ul>
|
||||
${this._templateResult.listeners.domains
|
||||
.sort()
|
||||
.map(
|
||||
(domain) => html`
|
||||
<li>
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.domain"
|
||||
)}</b
|
||||
>: ${domain}
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
${this._templateResult.listeners.entities
|
||||
.sort()
|
||||
.map(
|
||||
(entity_id) => html`
|
||||
<li>
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.entity"
|
||||
)}</b
|
||||
>: ${entity_id}
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>
|
||||
`
|
||||
: !this._templateResult?.listeners.time
|
||||
? html` <span class="all_listeners">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.no_listeners"
|
||||
)}
|
||||
</span>`
|
||||
"ui.panel.developer-tools.tabs.templates.result_type"
|
||||
)}:
|
||||
${resultType}
|
||||
<!-- prettier-ignore -->
|
||||
<pre class="rendered ${classMap({
|
||||
[resultType]: resultType,
|
||||
})}"
|
||||
>${type === "object"
|
||||
? JSON.stringify(this._templateResult.result, null, 2)
|
||||
: this._templateResult.result}</pre>
|
||||
${this._templateResult.listeners.time
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.time"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
${!this._templateResult.listeners
|
||||
? nothing
|
||||
: this._templateResult.listeners.all
|
||||
? html`
|
||||
<p class="all_listeners">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.all_listeners"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: this._templateResult.listeners.domains.length ||
|
||||
this._templateResult.listeners.entities.length
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.listeners"
|
||||
)}
|
||||
</p>
|
||||
<ul>
|
||||
${this._templateResult.listeners.domains
|
||||
.sort()
|
||||
.map(
|
||||
(domain) => html`
|
||||
<li>
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.domain"
|
||||
)}</b
|
||||
>: ${domain}
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
${this._templateResult.listeners.entities
|
||||
.sort()
|
||||
.map(
|
||||
(entity_id) => html`
|
||||
<li>
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.entity"
|
||||
)}</b
|
||||
>: ${entity_id}
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>
|
||||
`
|
||||
: !this._templateResult.listeners.time
|
||||
? html`<span class="all_listeners">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.no_listeners"
|
||||
)}
|
||||
</span>`
|
||||
: nothing}`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
@@ -276,6 +283,7 @@ class HaPanelDevTemplate extends LitElement {
|
||||
.render-pane {
|
||||
position: relative;
|
||||
max-width: 50%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.render-spinner {
|
||||
@@ -284,6 +292,11 @@ class HaPanelDevTemplate extends LitElement {
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.rendered {
|
||||
@apply --paper-font-code1;
|
||||
clear: both;
|
||||
@@ -297,10 +310,6 @@ class HaPanelDevTemplate extends LitElement {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.rendered.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.render-pane {
|
||||
max-width: 100%;
|
||||
@@ -323,6 +332,7 @@ class HaPanelDevTemplate extends LitElement {
|
||||
this._template = ev.detail.value;
|
||||
if (this._error) {
|
||||
this._error = undefined;
|
||||
this._errorLevel = undefined;
|
||||
}
|
||||
this._debounceRender();
|
||||
}
|
||||
@@ -330,24 +340,36 @@ class HaPanelDevTemplate extends LitElement {
|
||||
private async _subscribeTemplate() {
|
||||
this._rendering = true;
|
||||
await this._unsubscribeTemplate();
|
||||
this._templateResult = undefined;
|
||||
try {
|
||||
this._unsubRenderTemplate = subscribeRenderTemplate(
|
||||
this.hass.connection,
|
||||
(result) => {
|
||||
this._templateResult = result;
|
||||
this._error = undefined;
|
||||
if ("error" in result) {
|
||||
// We show the latest error, or a warning if there are no errors
|
||||
if (result.level === "ERROR" || this._errorLevel !== "ERROR") {
|
||||
this._error = result.error;
|
||||
this._errorLevel = result.level;
|
||||
}
|
||||
} else {
|
||||
this._templateResult = result;
|
||||
this._error = undefined;
|
||||
this._errorLevel = undefined;
|
||||
}
|
||||
},
|
||||
{
|
||||
template: this._template,
|
||||
timeout: 3,
|
||||
strict: true,
|
||||
report_errors: true,
|
||||
}
|
||||
);
|
||||
await this._unsubRenderTemplate;
|
||||
} catch (err: any) {
|
||||
this._error = "Unknown error";
|
||||
this._errorLevel = undefined;
|
||||
if (err.message) {
|
||||
this._error = err.message;
|
||||
this._errorLevel = undefined;
|
||||
this._templateResult = undefined;
|
||||
}
|
||||
this._unsubRenderTemplate = undefined;
|
||||
|
@@ -35,6 +35,7 @@ import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import "../components/hui-warning-element";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { GlanceCardConfig, GlanceConfigEntity } from "./types";
|
||||
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
|
||||
|
||||
@customElement("hui-glance-card")
|
||||
export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
@@ -121,28 +122,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.has("_config")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
|
||||
if (
|
||||
!this._configEntities ||
|
||||
!oldHass ||
|
||||
oldHass.themes !== this.hass!.themes ||
|
||||
oldHass.locale !== this.hass!.locale
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const entity of this._configEntities) {
|
||||
if (oldHass.states[entity.entity] !== this.hass!.states[entity.entity]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return hasConfigOrEntitiesChanged(this, changedProps);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
@@ -127,7 +127,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
this._unsubRenderTemplate = subscribeRenderTemplate(
|
||||
this.hass.connection,
|
||||
(result) => {
|
||||
this._templateResult = result;
|
||||
this._templateResult = result as RenderTemplateResult;
|
||||
},
|
||||
{
|
||||
template: this._config.content,
|
||||
@@ -139,6 +139,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
strict: true,
|
||||
}
|
||||
);
|
||||
await this._unsubRenderTemplate;
|
||||
} catch (_err) {
|
||||
this._templateResult = {
|
||||
result: this._config!.content,
|
||||
|
@@ -410,7 +410,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
ha-card {
|
||||
--mdc-ripple-color: var(--tile-color);
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
box-shadow 180ms ease-in-out,
|
||||
|
@@ -22,6 +22,11 @@ export function hasConfigChanged(
|
||||
oldHass.themes !== element.hass!.themes ||
|
||||
oldHass.locale !== element.hass!.locale ||
|
||||
oldHass.localize !== element.hass.localize ||
|
||||
oldHass.formatEntityState !== element.hass.formatEntityState ||
|
||||
oldHass.formatEntityAttributeName !==
|
||||
element.hass.formatEntityAttributeName ||
|
||||
oldHass.formatEntityAttributeValue !==
|
||||
element.hass.formatEntityAttributeValue ||
|
||||
oldHass.config.state !== element.hass.config.state
|
||||
) {
|
||||
return true;
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import "../tile-features/hui-alarm-modes-tile-feature";
|
||||
import "../tile-features/hui-climate-hvac-modes-tile-feature";
|
||||
import "../tile-features/hui-target-temperature-tile-feature";
|
||||
import "../tile-features/hui-cover-open-close-tile-feature";
|
||||
import "../tile-features/hui-cover-position-tile-feature";
|
||||
import "../tile-features/hui-cover-tilt-position-tile-feature";
|
||||
@@ -9,6 +8,8 @@ import "../tile-features/hui-fan-speed-tile-feature";
|
||||
import "../tile-features/hui-lawn-mower-commands-tile-feature";
|
||||
import "../tile-features/hui-light-brightness-tile-feature";
|
||||
import "../tile-features/hui-light-color-temp-tile-feature";
|
||||
import "../tile-features/hui-select-options-tile-feature";
|
||||
import "../tile-features/hui-target-temperature-tile-feature";
|
||||
import "../tile-features/hui-vacuum-commands-tile-feature";
|
||||
import "../tile-features/hui-water-heater-operation-modes-tile-feature";
|
||||
import { LovelaceTileFeatureConfig } from "../tile-features/types";
|
||||
@@ -28,6 +29,7 @@ const TYPES: Set<LovelaceTileFeatureConfig["type"]> = new Set([
|
||||
"lawn-mower-commands",
|
||||
"light-brightness",
|
||||
"light-color-temp",
|
||||
"select-options",
|
||||
"target-temperature",
|
||||
"vacuum-commands",
|
||||
"water-heater-operation-modes",
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import "@material/mwc-tab-bar/mwc-tab-bar";
|
||||
import "@material/mwc-tab/mwc-tab";
|
||||
import Fuse from "fuse.js";
|
||||
import {
|
||||
css,
|
||||
|
@@ -54,7 +54,7 @@ export class HuiEntityPickerTable extends LitElement {
|
||||
"ui.panel.lovelace.unused_entities.state_icon"
|
||||
),
|
||||
type: "icon",
|
||||
template: (_icon, entity: any) => html`
|
||||
template: (entity) => html`
|
||||
<state-badge
|
||||
@click=${this._handleEntityClicked}
|
||||
.hass=${this.hass!}
|
||||
@@ -68,9 +68,9 @@ export class HuiEntityPickerTable extends LitElement {
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
template: (name, entity: any) => html`
|
||||
template: (entity: any) => html`
|
||||
<div @click=${this._handleEntityClicked} style="cursor: pointer;">
|
||||
${name}
|
||||
${entity.name}
|
||||
${narrow
|
||||
? html` <div class="secondary">${entity.entity_id}</div> `
|
||||
: ""}
|
||||
@@ -103,10 +103,10 @@ export class HuiEntityPickerTable extends LitElement {
|
||||
sortable: true,
|
||||
width: "15%",
|
||||
hidden: narrow,
|
||||
template: (lastChanged: string) => html`
|
||||
template: (entity) => html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass!}
|
||||
.datetime=${lastChanged}
|
||||
.datetime=${entity.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user