Compare commits

..

49 Commits

Author SHA1 Message Date
Bram Kragten
7f2fcc73b5 Delay display of init page 2023-09-20 17:05:27 +02:00
Paul Bottein
4b5c7021ff Add select option tile feature (#17971) 2023-09-20 12:43:21 +02:00
Paul Bottein
3349031cbd Simplify data table template (#17825)
* Simplify data table template

* Fix backup and gallery
2023-09-20 12:09:44 +02:00
renovate[bot]
5e107d43d7 Update babel monorepo to v7.22.20 (#17965)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-19 14:36:07 -04:00
Steve Repsher
e46f2cd9bf Adjust password manager polyfill for form injections (#17830) 2023-09-19 14:51:38 +02:00
Steve Repsher
713ebfcc22 Enable shipped proposals in Babel (#17909) 2023-09-19 13:56:55 +02:00
renovate[bot]
46e4eafe95 Update dependency @types/babel__plugin-transform-runtime to v7.9.3 (#17957) 2023-09-19 07:45:50 -04:00
renovate[bot]
e6fd18e23b Update dependency @types/js-yaml to v4.0.6 (#17958) 2023-09-19 07:43:55 -04:00
renovate[bot]
71cd71dfd5 Update formatjs monorepo (#17890)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-18 12:50:43 -04:00
renovate[bot]
1019ccfd26 Update dependency @lrnwebcomponents/simple-tooltip to v7.0.18 (#17905)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-18 12:41:29 -04:00
renovate[bot]
577c1d8522 Update dependency @lit-labs/context to v0.4.1 (#17935)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-18 12:35:46 -04:00
renovate[bot]
63f0b469cc Update dependency vis-data to v7.1.7 (#17939) 2023-09-18 12:25:23 -04:00
renovate[bot]
e688417863 Update dependency @babel/core to v7.22.19 (#17948) 2023-09-18 08:27:41 -04:00
renovate[bot]
a19633e2d4 Update CodeMirror (#17944) 2023-09-18 08:21:31 -04:00
renovate[bot]
8797142cca Update dependency sinon to v16 (#17940) 2023-09-18 08:15:40 -04:00
Madelena Mak
2a7403b6fd Added new logo design guidelines (#17951)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2023-09-17 22:29:53 +02:00
renovate[bot]
22efe14149 Update dependency @types/tar to v6.1.6 (#17931)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-15 18:57:25 -04:00
renovate[bot]
7cce24bcd1 Update typescript-eslint monorepo to v6.7.0 (#17923) 2023-09-14 20:25:46 -04:00
renovate[bot]
b8f0bb66cd Update dependency @codemirror/view to v6.18.1 (#17922)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-14 13:11:12 -04:00
renovate[bot]
b950f990b4 Update dependency @material/web to v1.0.0-pre.17 (#17882)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-13 10:11:13 +02:00
renovate[bot]
b511e7a37d Update vaadinWebComponents monorepo to v24.1.7 (#17894) 2023-09-12 20:38:23 -04:00
renovate[bot]
50f4b78f2e Update dependency ua-parser-js to v1.0.36 (#17912)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-12 11:45:17 -04:00
renovate[bot]
7b0b4cdfe4 Update dependency @codemirror/view to v6.18.0 (#17865) 2023-09-11 20:59:49 -04:00
renovate[bot]
c60e5c4c61 Update dependency eslint to v8.49.0 (#17904) 2023-09-11 20:41:20 -04:00
karwosts
709a63e6da Fix schedule helper form when resizing or dragging to or past midnight (#17900) 2023-09-11 22:54:45 +02:00
dependabot[bot]
f689eed073 Bump actions/checkout from 3.6.0 to 4.0.0 (#17891)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.6.0 to 4.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.6.0...v4.0.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-11 15:22:18 -04:00
renovate[bot]
cd55eee2fc Update dependency @babel/core to v7.22.17 (#17899)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-11 15:19:05 -04:00
dependabot[bot]
cf27e68748 Bump actions/cache from 3.3.1 to 3.3.2 (#17892)
Bumps [actions/cache](https://github.com/actions/cache) from 3.3.1 to 3.3.2.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.3.1...v3.3.2)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-11 12:24:36 -04:00
Bram Kragten
472ed2fe82 Bumped version to 20230911.0 2023-09-11 18:19:34 +02:00
Paul Bottein
d0a60984ed Allow user to theme all cover states in tile card and more info. (#17898) 2023-09-11 18:18:39 +02:00
Bram Kragten
24d401061c Show error when set state fails (#17850) 2023-09-11 18:13:36 +02:00
Bram Kragten
2352d05573 Ignore errors during logging (#17893) 2023-09-11 13:24:08 +02:00
renovate[bot]
87d53e38c4 Update dependency tar to v6.2.0 (#17861)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-10 19:21:53 -04:00
renovate[bot]
db3c535884 Update dependency core-js to v3.32.2 (#17887)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-10 19:18:23 -04:00
renovate[bot]
158b24f902 Update dependency luxon to v3.4.3 (#17872)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-10 19:16:40 -04:00
Joakim Sørensen
19c4ed4690 Show alternative names in certificate dialog (#17839) 2023-09-08 19:55:27 +02:00
Bram Kragten
eae4ca1271 if select has multiple, make init value an array (#17868)
if multiple, make it an array
2023-09-08 19:46:40 +02:00
Bram Kragten
0276430ab5 Bumped version to 20230908.0 2023-09-08 15:55:22 +02:00
karwosts
db7caf1c32 Fix default formatEntityState function (#17852) 2023-09-08 15:54:54 +02:00
karwosts
7176a51fec update hasChanged for formatEntityState, fix glance card shouldUpdate (#17854) 2023-09-08 15:54:11 +02:00
Bram Kragten
4a6539d75b Clear template result when template changes (#17849) 2023-09-07 20:48:57 -04:00
renovate[bot]
850699ea70 Update dependency @types/chromecast-caf-sender to v1.0.6 (#17857) 2023-09-08 00:01:27 +00:00
renovate[bot]
c17cc22f88 Update typescript-eslint monorepo to v6.6.0 (#17858) 2023-09-07 19:59:46 -04:00
renovate[bot]
9e3f2d5cb7 Update babel monorepo to v7.22.15 (#17853) 2023-09-07 19:56:59 -04:00
renovate[bot]
0677c9c7b0 Update dependency @types/chromecast-caf-receiver to v6.0.10 (#17856) 2023-09-07 19:47:22 -04:00
Bram Kragten
af7e385884 Fix tooltip graph overflow (#17848) 2023-09-07 19:12:35 +02:00
Bram Kragten
ba88fef09b Bumped version to 20230906.1 2023-09-06 13:41:40 +02:00
Paul Bottein
ad0e59c8f4 Fix no listener margin in flow preview template (#17837) 2023-09-06 13:41:16 +02:00
Bram Kragten
14e6f5e8ca Subscribe to config entries in helper config (#17835) 2023-09-06 13:39:20 +02:00
78 changed files with 1774 additions and 1369 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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: |

View File

@@ -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",

View File

@@ -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

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -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)
![Logo](/images/logo.png)
![Logo](/images/brand/logo.png)
## 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
![Logo variants](/images/logo-variants.png)
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.
![Clearspace](/images/clearspace.png)
# Usage
The default variation is the static colored wordmark in horizontal layout and dark text on a light background.
## Layout variations
![Logo layout variants](/images/brand/logo-layout-variants.png)
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.
![Space clearance for the wordmark](/images/brand/logo-exclusion-zone.png)
## 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.

View File

@@ -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,

View File

@@ -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),

View File

@@ -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,11 +153,11 @@
"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",
@@ -169,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",
@@ -183,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",
@@ -234,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",

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20230906.0"
version = "20230911.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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>
`;
})}

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -61,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] = typeof val === "string" ? val : val.value;
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] = {

View File

@@ -11,6 +11,7 @@ export interface CertificateInformation {
common_name: string;
expire_date: string;
fingerprint: string;
alternative_names: string[];
}
export interface CloudPreferences {

View File

@@ -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 },

View File

@@ -110,11 +110,11 @@ class FlowPreviewTemplate extends LitElement {
</ul>
`
: !this._listeners.time
? html`<span class="all_listeners">
? html`<p class="all_listeners">
${this.hass.localize(
"ui.dialogs.helper_settings.template.no_listeners"
)}
</span>`
</p>`
: nothing} `;
}
@@ -128,7 +128,7 @@ class FlowPreviewTemplate extends LitElement {
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 },

View File

@@ -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",

View File

@@ -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,
})}

View File

@@ -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,
})}

File diff suppressed because one or more lines are too long

View File

@@ -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;

View File

@@ -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>`
);

View File

@@ -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)}`,
},
};

View File

@@ -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

View File

@@ -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}

View File

@@ -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", {

View File

@@ -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">

View File

@@ -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")) {

View File

@@ -1336,7 +1336,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
}
private async _showOptionsFlow() {
showOptionsFlowDialog(this, this.helperConfigEntry!, null);
showOptionsFlowDialog(this, this.helperConfigEntry!);
}
private _switchAsDomainsSorted = memoizeOne(

View File

@@ -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"
)

View File

@@ -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();
}
}

View File

@@ -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, {});
}
}

View File

@@ -1024,7 +1024,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
showOptionsFlowDialog(
this,
ev.target.closest(".config_entry").configEntry,
this._manifest
{ manifest: this._manifest }
);
}

View File

@@ -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%",
},

View File

@@ -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;"

View File

@@ -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,
},
}

View File

@@ -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>
`,

View File

@@ -68,12 +68,12 @@ export class HaConfigLovelaceDashboards extends LitElement {
"ui.panel.config.lovelace.dashboards.picker.headers.icon"
),
type: "icon",
template: (icon: DataTableItem["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}`
@@ -91,9 +91,9 @@ export class HaConfigLovelaceDashboards extends LitElement {
sortable: true,
filterable: true,
grows: true,
template: (title: DataTableItem["title"], dashboard) => {
template: (dashboard) => {
const titleTemplate = html`
${title}
${dashboard.title}
${dashboard.default
? html`
<ha-svg-icon
@@ -132,10 +132,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
sortable: true,
filterable: true,
width: "20%",
template: (mode: DataTableItem["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)) {
@@ -155,8 +155,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
sortable: true,
type: "icon",
width: "100px",
template: (requireAdmin: DataTableItem["require_admin"]) =>
requireAdmin
template: (dashboard) =>
dashboard.require_admin
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
: html``,
};
@@ -166,8 +166,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
),
type: "icon",
width: "121px",
template: (sidebar: DataTableItem["show_in_sidebar"]) =>
sidebar
template: (dashboard) =>
dashboard.show_in_sidebar
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
: html``,
};
@@ -180,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"
@@ -193,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

View File

@@ -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: LovelaceResource["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}
`,
},
})

View File

@@ -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

View File

@@ -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`

View File

@@ -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) => ({

View File

@@ -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]) =>

View File

@@ -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: {

View File

@@ -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() {

View File

@@ -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

View File

@@ -340,6 +340,7 @@ class HaPanelDevTemplate extends LitElement {
private async _subscribeTemplate() {
this._rendering = true;
await this._unsubscribeTemplate();
this._templateResult = undefined;
try {
this._unsubRenderTemplate = subscribeRenderTemplate(
this.hass.connection,

View File

@@ -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() {

View File

@@ -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,

View File

@@ -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;

View File

@@ -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",

View File

@@ -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>
`,

View File

@@ -35,6 +35,7 @@ import { supportsFanSpeedTileFeature } from "../../tile-features/hui-fan-speed-t
import { supportsLawnMowerCommandTileFeature } from "../../tile-features/hui-lawn-mower-commands-tile-feature";
import { supportsLightBrightnessTileFeature } from "../../tile-features/hui-light-brightness-tile-feature";
import { supportsLightColorTempTileFeature } from "../../tile-features/hui-light-color-temp-tile-feature";
import { supportsSelectOptionTileFeature } from "../../tile-features/hui-select-options-tile-feature";
import { supportsTargetTemperatureTileFeature } from "../../tile-features/hui-target-temperature-tile-feature";
import { supportsVacuumCommandTileFeature } from "../../tile-features/hui-vacuum-commands-tile-feature";
import { supportsWaterHeaterOperationModesTileFeature } from "../../tile-features/hui-water-heater-operation-modes-tile-feature";
@@ -46,7 +47,6 @@ type SupportsFeature = (stateObj: HassEntity) => boolean;
const FEATURE_TYPES: FeatureType[] = [
"alarm-modes",
"climate-hvac-modes",
"target-temperature",
"cover-open-close",
"cover-position",
"cover-tilt-position",
@@ -55,6 +55,8 @@ const FEATURE_TYPES: FeatureType[] = [
"lawn-mower-commands",
"light-brightness",
"light-color-temp",
"select-options",
"target-temperature",
"vacuum-commands",
"water-heater-operation-modes",
];
@@ -83,6 +85,7 @@ const SUPPORTS_FEATURE_TYPES: Record<FeatureType, SupportsFeature | undefined> =
"vacuum-commands": supportsVacuumCommandTileFeature,
"water-heater-operation-modes":
supportsWaterHeaterOperationModesTileFeature,
"select-options": supportsSelectOptionTileFeature,
};
const CUSTOM_FEATURE_ENTRIES: Record<

View File

@@ -64,14 +64,16 @@ class HuiCoverPositionTileFeature
const value = Math.max(Math.round(percentage), 0);
const forcedState = this.stateObj.state === "closed" ? "open" : undefined;
const openColor = stateColorCss(this.stateObj, "open");
const color = this.color
? computeCssColor(this.color)
: stateColorCss(this.stateObj, forcedState);
: stateColorCss(this.stateObj);
const style = {
"--color": color,
// Use open color for inactive state to avoid grey slider that looks disabled
"--state-cover-inactive-color": openColor,
};
return html`

View File

@@ -64,14 +64,16 @@ class HuiCoverTiltPositionTileFeature
const value = Math.max(Math.round(percentage), 0);
const forcedState = this.stateObj.state === "closed" ? "open" : undefined;
const openColor = stateColorCss(this.stateObj, "open");
const color = this.color
? computeCssColor(this.color)
: stateColorCss(this.stateObj, forcedState);
: stateColorCss(this.stateObj);
const style = {
"--color": color,
// Use open color for inactive state to avoid grey slider that looks disabled
"--state-cover-inactive-color": openColor,
};
return html`

View File

@@ -0,0 +1,154 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-control-select-menu";
import type { HaControlSelectMenu } from "../../../components/ha-control-select-menu";
import { UNAVAILABLE } from "../../../data/entity";
import { InputSelectEntity } from "../../../data/input_select";
import { SelectEntity } from "../../../data/select";
import { HomeAssistant } from "../../../types";
import { LovelaceTileFeature } from "../types";
import { SelectOptionsTileFeatureConfig } from "./types";
export const supportsSelectOptionTileFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "select" || domain === "input_select";
};
@customElement("hui-select-options-tile-feature")
class HuiSelectOptionsTileFeature
extends LitElement
implements LovelaceTileFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?:
| SelectEntity
| InputSelectEntity;
@state() private _config?: SelectOptionsTileFeatureConfig;
@state() _currentOption?: string;
@query("ha-control-select-menu", true)
private _haSelect!: HaControlSelectMenu;
static getStubConfig(): SelectOptionsTileFeatureConfig {
return {
type: "select-options",
};
}
public setConfig(config: SelectOptionsTileFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp);
if (changedProp.has("stateObj") && this.stateObj) {
this._currentOption = this.stateObj.state;
}
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
this.hass &&
this.hass.formatEntityAttributeValue !==
oldHass?.formatEntityAttributeValue
) {
this._haSelect.layoutOptions();
}
}
}
private async _valueChanged(ev: CustomEvent) {
const option = (ev.target as any).value as string;
if (option === this.stateObj!.state) return;
const oldOption = this.stateObj!.state;
this._currentOption = option;
try {
await this._setOption(option);
} catch (err) {
this._currentOption = oldOption;
}
}
private async _setOption(option: string) {
const domain = computeDomain(this.stateObj!.entity_id);
await this.hass!.callService(domain, "select_option", {
entity_id: this.stateObj!.entity_id,
option: option,
});
}
protected render() {
if (
!this._config ||
!this.hass ||
!this.stateObj ||
!supportsSelectOptionTileFeature(this.stateObj)
) {
return nothing;
}
const stateObj = this.stateObj;
return html`
<div class="container">
<ha-control-select-menu
show-arrow
hide-label
.label=${"Option"}
.value=${stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._valueChanged}
@closed=${stopPropagation}
>
${stateObj.attributes.options!.map(
(option) => html`
<ha-list-item .value=${option}>
${this.hass!.formatEntityState(stateObj, option)}
</ha-list-item>
`
)}
</ha-control-select-menu>
</div>
`;
}
static get styles() {
return css`
ha-control-select-menu {
box-sizing: border-box;
--control-select-menu-height: 40px;
--control-select-menu-border-radius: 10px;
line-height: 1.2;
display: block;
width: 100%;
}
.container {
padding: 0 12px 12px 12px;
width: auto;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-select-options-tile-feature": HuiSelectOptionsTileFeature;
}
}

View File

@@ -40,6 +40,10 @@ export interface ClimateHvacModesTileFeatureConfig {
hvac_modes?: HvacMode[];
}
export interface SelectOptionsTileFeatureConfig {
type: "select-options";
}
export interface TargetTemperatureTileFeatureConfig {
type: "target-temperature";
}
@@ -86,7 +90,8 @@ export type LovelaceTileFeatureConfig =
| LightColorTempTileFeatureConfig
| VacuumCommandsTileFeatureConfig
| TargetTemperatureTileFeatureConfig
| WaterHeaterOperationModesTileFeatureConfig;
| WaterHeaterOperationModesTileFeatureConfig
| SelectOptionsTileFeatureConfig;
export type LovelaceTileFeatureContext = {
entity_id?: string;

View File

@@ -177,10 +177,10 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
// @ts-ignore
this._loadFragmentTranslations(this.hass?.language, fragment),
formatEntityState: (stateObj, state) =>
(state !== null ? state : stateObj.state) ?? "",
(state != null ? state : stateObj.state) ?? "",
formatEntityAttributeName: (_stateObj, attribute) => attribute,
formatEntityAttributeValue: (stateObj, attribute, value) =>
value !== null ? value : stateObj.attributes[attribute] ?? "",
value != null ? value : stateObj.attributes[attribute] ?? "",
...getState(),
...this._pendingHass,
};

View File

@@ -40,30 +40,46 @@ export const loggingMixin = <T extends Constructor<HassBaseEl>>(
ev.stopPropagation();
return;
}
const { createLogMessage } = await import("../resources/log-message");
this._writeLog({
// The error object from browsers includes the message and a stack trace,
// so use the data in the error event just as fallback
message: await createLogMessage(
let message;
try {
const { createLogMessage } = await import("../resources/log-message");
message = await createLogMessage(
ev.error,
"Uncaught error",
ev.message,
`@${ev.filename}:${ev.lineno}:${ev.colno}`
),
});
);
await this._writeLog({
// The error object from browsers includes the message and a stack trace,
// so use the data in the error event just as fallback
message,
});
} catch (e) {
// eslint-disable-next-line no-console
console.error("Error during logging error:", message, e);
// catch errors during logging so we don't get into a loop
}
});
window.addEventListener("unhandledrejection", async (ev) => {
if (!this.hass?.connected) {
return;
}
const { createLogMessage } = await import("../resources/log-message");
this._writeLog({
message: await createLogMessage(
let message;
try {
const { createLogMessage } = await import("../resources/log-message");
message = await createLogMessage(
ev.reason,
"Unhandled promise rejection"
),
level: "debug",
});
);
await this._writeLog({
message,
level: "debug",
});
} catch (e) {
// eslint-disable-next-line no-console
console.error("Error during logging error:", message, e);
// catch errors during logging so we don't get into a loop
}
});
}
@@ -75,7 +91,7 @@ export const loggingMixin = <T extends Constructor<HassBaseEl>>(
}
private _writeLog(log: WriteLogParams) {
this.hass?.callService("system_log", "write", {
return this.hass?.callService("system_log", "write", {
logger: `frontend.${
__DEV__ ? "js_dev" : "js"
}.${__BUILD__}.${__VERSION__.replace(".", "")}`,

View File

@@ -17,15 +17,15 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) => {
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (this.hass) {
if (
this.hass.localize !== oldHass?.localize ||
if (
this.hass &&
(!oldHass ||
this.hass.localize !== oldHass.localize ||
this.hass.locale !== oldHass.locale ||
this.hass.config !== oldHass.config ||
this.hass.entities !== oldHass.entities
) {
this._updateStateDisplay();
}
this.hass.entities !== oldHass.entities)
) {
this._updateStateDisplay();
}
}

View File

@@ -3183,6 +3183,7 @@
}
},
"dialog_certificate": {
"alternative_names": "Alternative names:",
"certificate_information": "Certificate information",
"certificate_expiration_date": "Certificate expiration date:",
"will_be_auto_renewed": "will be automatically renewed",

View File

@@ -7,9 +7,9 @@ export const removeLaunchScreen = () => {
}
};
export const renderLaunchScreenInfoBox = (content: TemplateResult) => {
const infoBoxElement = document.getElementById("ha-launch-screen-info-box");
if (infoBoxElement) {
render(content, infoBoxElement);
export const renderLaunchScreen = (content: TemplateResult) => {
const launchScreen = document.getElementById("ha-launch-screen");
if (launchScreen) {
render(content, launchScreen);
}
};

1044
yarn.lock

File diff suppressed because it is too large Load Diff