Merge branch 'dev' into dependabot-dedupe

This commit is contained in:
Steve Repsher 2023-02-14 17:20:09 +00:00
commit 6ba567c54e
No known key found for this signature in database
GPG Key ID: 776C4F2DACF6131B
282 changed files with 10051 additions and 6814 deletions

View File

@ -1,5 +1,5 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.9 FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.10
ENV \ ENV \
DEBIAN_FRONTEND=noninteractive \ DEBIAN_FRONTEND=noninteractive \

View File

@ -5,6 +5,7 @@
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:wc/recommended", "plugin:wc/recommended",
"plugin:lit/all", "plugin:lit/all",
"plugin:lit-a11y/recommended",
"prettier" "prettier"
], ],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
@ -65,7 +66,10 @@
"import/extensions": [ "import/extensions": [
"error", "error",
"ignorePackages", "ignorePackages",
{ "ts": "never", "js": "never" } {
"ts": "never",
"js": "never"
}
], ],
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"], "no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"object-curly-newline": "off", "object-curly-newline": "off",
@ -112,7 +116,15 @@
], ],
"unused-imports/no-unused-imports": "error", "unused-imports/no-unused-imports": "error",
"lit/attribute-value-entities": "off", "lit/attribute-value-entities": "off",
"lit/no-template-map": "off" "lit/no-template-map": "off",
"lit/no-native-attributes": "warn",
"lit/no-this-assign-in-render": "warn",
"lit/prefer-nothing": "warn",
"lit-a11y/click-events-have-key-events": ["off"],
"lit-a11y/no-autofocus": "off",
"lit-a11y/alt-text": "warn",
"lit-a11y/anchor-is-valid": "warn",
"lit-a11y/role-has-required-aria-attrs": "warn"
}, },
"plugins": ["disable", "unused-imports"], "plugins": ["disable", "unused-imports"],
"processor": "disable/disable" "processor": "disable/disable"

View File

@ -10,5 +10,12 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "daily"
time: "06:00" time: "03:00"
open-pull-requests-limit: 5 open-pull-requests-limit: 10
labels:
- "dependencies"
ignore:
# Ignore rollup and plugins until everything else is updated
- dependency-name: "*rollup*"
- dependency-name: "@rollup/*"
- dependency-name: "serve"

View File

@ -33,9 +33,7 @@ jobs:
cache: yarn cache: yarn
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install --immutable
env:
CI: true
- name: Build Cast - name: Build Cast
run: ./node_modules/.bin/gulp build-cast run: ./node_modules/.bin/gulp build-cast
@ -71,9 +69,7 @@ jobs:
cache: yarn cache: yarn
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install --immutable
env:
CI: true
- name: Build Cast - name: Build Cast
run: ./node_modules/.bin/gulp build-cast run: ./node_modules/.bin/gulp build-cast
@ -87,4 +83,4 @@ jobs:
args: deploy --dir=cast/dist --prod args: deploy --dir=cast/dist --prod
env: env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}

View File

@ -38,7 +38,7 @@ jobs:
- name: Build resources - name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Run eslint - name: Run eslint
run: yarn run lint:eslint run: yarn run lint:eslint --quiet
- name: Run tsc - name: Run tsc
run: yarn run lint:types run: yarn run lint:types
- name: Run prettier - name: Run prettier

View File

@ -34,9 +34,7 @@ jobs:
cache: yarn cache: yarn
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install --immutable
env:
CI: true
- name: Build Demo - name: Build Demo
run: ./node_modules/.bin/gulp build-demo run: ./node_modules/.bin/gulp build-demo
@ -72,9 +70,7 @@ jobs:
cache: yarn cache: yarn
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install --immutable
env:
CI: true
- name: Build Demo - name: Build Demo
run: ./node_modules/.bin/gulp build-demo run: ./node_modules/.bin/gulp build-demo
@ -88,4 +84,4 @@ jobs:
args: deploy --dir=demo/dist --prod args: deploy --dir=demo/dist --prod
env: env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }}

View File

@ -26,9 +26,7 @@ jobs:
cache: yarn cache: yarn
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install --immutable
env:
CI: true
- name: Build Gallery - name: Build Gallery
run: ./node_modules/.bin/gulp build-gallery run: ./node_modules/.bin/gulp build-gallery

View File

@ -31,9 +31,7 @@ jobs:
cache: yarn cache: yarn
- name: Install dependencies - name: Install dependencies
run: yarn install run: yarn install --immutable
env:
CI: true
- name: Build Gallery - name: Build Gallery
run: ./node_modules/.bin/gulp build-gallery run: ./node_modules/.bin/gulp build-gallery

4
.gitignore vendored
View File

@ -8,7 +8,7 @@ dist/
/translations/ /translations/
# yarn # yarn
.yarn/** .yarn/*
!.yarn/patches !.yarn/patches
!.yarn/releases !.yarn/releases
!.yarn/plugins !.yarn/plugins
@ -31,7 +31,7 @@ pip-selfcheck.json
.venv .venv
# vscode # vscode
.vscode/** .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/tasks.json !.vscode/tasks.json

View File

@ -1,29 +0,0 @@
diff --git a/polyfillLoaders/EventTarget.js b/polyfillLoaders/EventTarget.js
index 4e18ade7ba485849f17f28c94c42f0e0e01ac387..8f34f4f646c7f7becc208fb5a546c96034fc74dc 100644
--- a/polyfillLoaders/EventTarget.js
+++ b/polyfillLoaders/EventTarget.js
@@ -6,16 +6,15 @@
let _ET;
let ET;
export default async function EventTarget() {
- return ET || init();
+ return ET || init();
}
async function init() {
- _ET = window.EventTarget;
- try {
- new _ET();
- }
- catch (_a) {
- _ET = (await import('event-target-shim')).EventTarget;
- }
- return (ET = _ET);
+ _ET = window.EventTarget;
+ try {
+ new _ET();
+ } catch (_a) {
+ _ET = (await import("event-target-shim")).default.EventTarget;
+ }
+ return (ET = _ET);
}
//# sourceMappingURL=EventTarget.js.map

View File

@ -1,12 +0,0 @@
diff --git a/mwc-icon-button-base.js b/mwc-icon-button-base.js
index 45cdaab93ccc0a6daaaaabc01266dcdc32e46bfd..b3ea5b541597308d85f86ce6c23fd00785fda835 100644
--- a/mwc-icon-button-base.js
+++ b/mwc-icon-button-base.js
@@ -63,7 +63,6 @@ export class IconButtonBase extends LitElement {
@touchend="${this.handleRippleDeactivate}"
@touchcancel="${this.handleRippleDeactivate}"
>${this.renderRipple()}
- <i class="material-icons">${this.icon}</i>
<span
><slot></slot
></span>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

823
.yarn/releases/yarn-3.3.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@ -6,4 +6,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools" spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.2.3.cjs yarnPath: .yarn/releases/yarn-3.3.1.cjs

View File

@ -67,7 +67,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({
"@babel/preset-env", "@babel/preset-env",
{ {
useBuiltIns: "entry", useBuiltIns: "entry",
corejs: "3.15", corejs: { version: "3.27", proposals: true },
bugfixes: true, bugfixes: true,
}, },
], ],

View File

@ -1,36 +1,40 @@
const del = require("del"); const del = import("del");
const gulp = require("gulp"); const gulp = require("gulp");
const paths = require("../paths"); const paths = require("../paths");
require("./translations"); require("./translations");
gulp.task( gulp.task(
"clean", "clean",
gulp.parallel("clean-translations", () => gulp.parallel("clean-translations", async () =>
del([paths.app_output_root, paths.build_dir]) (await del).deleteSync([paths.app_output_root, paths.build_dir])
) )
); );
gulp.task( gulp.task(
"clean-demo", "clean-demo",
gulp.parallel("clean-translations", () => gulp.parallel("clean-translations", async () =>
del([paths.demo_output_root, paths.build_dir]) (await del).deleteSync([paths.demo_output_root, paths.build_dir])
) )
); );
gulp.task( gulp.task(
"clean-cast", "clean-cast",
gulp.parallel("clean-translations", () => gulp.parallel("clean-translations", async () =>
del([paths.cast_output_root, paths.build_dir]) (await del).deleteSync([paths.cast_output_root, paths.build_dir])
) )
); );
gulp.task("clean-hassio", () => gulp.task("clean-hassio", async () =>
del([paths.hassio_output_root, paths.build_dir]) (await del).deleteSync([paths.hassio_output_root, paths.build_dir])
); );
gulp.task( gulp.task(
"clean-gallery", "clean-gallery",
gulp.parallel("clean-translations", () => gulp.parallel("clean-translations", async () =>
del([paths.gallery_output_root, paths.gallery_build, paths.build_dir]) (await del).deleteSync([
paths.gallery_output_root,
paths.gallery_build,
paths.build_dir,
])
) )
); );

View File

@ -1,9 +1,9 @@
// Task to download the latest Lokalise translations from the nightly workflow artifacts // Task to download the latest Lokalise translations from the nightly workflow artifacts
const del = import("del");
const fs = require("fs/promises"); const fs = require("fs/promises");
const path = require("path"); const path = require("path");
const process = require("process"); const process = require("process");
const del = require("del");
const gulp = require("gulp"); const gulp = require("gulp");
const jszip = require("jszip"); const jszip = require("jszip");
const tar = require("tar"); const tar = require("tar");
@ -17,8 +17,8 @@ const WORKFLOW_NAME = "nightly.yaml";
const ARTIFACT_NAME = "translations"; const ARTIFACT_NAME = "translations";
const CLIENT_ID = "Iv1.3914e28cb27834d1"; const CLIENT_ID = "Iv1.3914e28cb27834d1";
const EXTRACT_DIR = "translations"; const EXTRACT_DIR = "translations";
const TOKEN_FILE = path.join(EXTRACT_DIR, "token.json"); const TOKEN_FILE = path.posix.join(EXTRACT_DIR, "token.json");
const ARTIFACT_FILE = path.join(EXTRACT_DIR, "artifact.json"); const ARTIFACT_FILE = path.posix.join(EXTRACT_DIR, "artifact.json");
let allowTokenSetup = false; let allowTokenSetup = false;
gulp.task("allow-setup-fetch-nightly-translations", (done) => { gulp.task("allow-setup-fetch-nightly-translations", (done) => {
@ -137,7 +137,11 @@ gulp.task("fetch-nightly-translations", async function () {
// Remove the current translations // Remove the current translations
const deleteCurrent = Promise.all(writings).then( const deleteCurrent = Promise.all(writings).then(
del([`${EXTRACT_DIR}/*`, `!${ARTIFACT_FILE}`, `!${TOKEN_FILE}`]) (await del).deleteAsync([
`${EXTRACT_DIR}/*`,
`!${ARTIFACT_FILE}`,
`!${TOKEN_FILE}`,
])
); );
// Get the download URL and follow the redirect to download (stored as ArrayBuffer) // Get the download URL and follow the redirect to download (stored as ArrayBuffer)

View File

@ -1,4 +1,4 @@
const del = require("del"); const del = import("del");
const path = require("path"); const path = require("path");
const gulp = require("gulp"); const gulp = require("gulp");
const fs = require("fs"); const fs = require("fs");
@ -6,7 +6,7 @@ const paths = require("../paths");
const outDir = "build/locale-data"; const outDir = "build/locale-data";
gulp.task("clean-locale-data", () => del([outDir])); gulp.task("clean-locale-data", async () => (await del).deleteSync([outDir]));
gulp.task("ensure-locale-data-build-dir", (done) => { gulp.task("ensure-locale-data-build-dir", (done) => {
if (!fs.existsSync(outDir)) { if (!fs.existsSync(outDir)) {

View File

@ -1,5 +1,5 @@
const del = import("del");
const crypto = require("crypto"); const crypto = require("crypto");
const del = require("del");
const path = require("path"); const path = require("path");
const source = require("vinyl-source-stream"); const source = require("vinyl-source-stream");
const vinylBuffer = require("vinyl-buffer"); const vinylBuffer = require("vinyl-buffer");
@ -13,7 +13,7 @@ const { mapFiles } = require("../util");
const env = require("../env"); const env = require("../env");
const paths = require("../paths"); const paths = require("../paths");
require("./fetch-nightly_translations"); require("./fetch-nightly-translations");
const inFrontendDir = "translations/frontend"; const inFrontendDir = "translations/frontend";
const inBackendDir = "translations/backend"; const inBackendDir = "translations/backend";
@ -120,7 +120,7 @@ function lokaliseTransform(data, original, file) {
return output; return output;
} }
gulp.task("clean-translations", () => del([workDir])); gulp.task("clean-translations", async () => (await del).deleteSync([workDir]));
gulp.task("ensure-translations-build-dir", (done) => { gulp.task("ensure-translations-build-dir", (done) => {
if (!fs.existsSync(workDir)) { if (!fs.existsSync(workDir)) {

View File

@ -22,7 +22,11 @@ class HcLayout extends LitElement {
return html` return html`
<ha-card> <ha-card>
<div class="layout"> <div class="layout">
<img class="hero" src="/images/google-nest-hub.png" /> <img
class="hero"
alt="A Google Nest Hub with a Home Assistant dashboard on its screen"
src="/images/google-nest-hub.png"
/>
<h1 class="card-header"> <h1 class="card-header">
Home Assistant Cast${this.subtitle ? ` ${this.subtitle}` : ""} Home Assistant Cast${this.subtitle ? ` ${this.subtitle}` : ""}
${this.auth ${this.auth

View File

@ -12,6 +12,7 @@ class HcLaunchScreen extends LitElement {
return html` return html`
<div class="container"> <div class="container">
<img <img
alt="Home Assistant logo on left, Nabu Casa logo on right, and red heart in center"
src="https://www.home-assistant.io/images/blog/2018-09-thinking-big/social.png" src="https://www.home-assistant.io/images/blog/2018-09-thinking-big/social.png"
/> />
<div class="status"> <div class="status">

View File

@ -1,4 +1,5 @@
// Compat needs to be first import // Compat needs to be first import
import "../../src/resources/compatibility";
import { isNavigationClick } from "../../src/common/dom/is-navigation-click"; import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
import { navigate } from "../../src/common/navigate"; import { navigate } from "../../src/common/navigate";
import { import {
@ -6,7 +7,6 @@ import {
provideHass, provideHass,
} from "../../src/fake_data/provide_hass"; } from "../../src/fake_data/provide_hass";
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant"; import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
import "../../src/resources/compatibility";
import { HomeAssistant } from "../../src/types"; import { HomeAssistant } from "../../src/types";
import { selectedDemoConfig } from "./configs/demo-configs"; import { selectedDemoConfig } from "./configs/demo-configs";
import { mockAuth } from "./stubs/auth"; import { mockAuth } from "./stubs/auth";
@ -71,6 +71,7 @@ class HaDemo extends HomeAssistantAppEl {
entity_category: null, entity_category: null,
has_entity_name: false, has_entity_name: false,
unique_id: "co2_intensity", unique_id: "co2_intensity",
options: null,
}, },
{ {
config_entry_id: "co2signal", config_entry_id: "co2signal",
@ -86,6 +87,7 @@ class HaDemo extends HomeAssistantAppEl {
entity_category: null, entity_category: null,
has_entity_name: false, has_entity_name: false,
unique_id: "grid_fossil_fuel_percentage", unique_id: "grid_fossil_fuel_percentage",
options: null,
}, },
]); ]);

View File

@ -15,6 +15,7 @@ import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
const generateMeanStatistics = ( const generateMeanStatistics = (
start: Date, start: Date,
end: Date, end: Date,
// eslint-disable-next-line @typescript-eslint/default-param-last
period: "5minute" | "hour" | "day" | "month" = "hour", period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number, initValue: number,
maxDiff: number maxDiff: number
@ -51,6 +52,7 @@ const generateMeanStatistics = (
const generateSumStatistics = ( const generateSumStatistics = (
start: Date, start: Date,
end: Date, end: Date,
// eslint-disable-next-line @typescript-eslint/default-param-last
period: "5minute" | "hour" | "day" | "month" = "hour", period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number, initValue: number,
maxDiff: number maxDiff: number
@ -86,6 +88,7 @@ const generateSumStatistics = (
const generateCurvedStatistics = ( const generateCurvedStatistics = (
start: Date, start: Date,
end: Date, end: Date,
// eslint-disable-next-line @typescript-eslint/default-param-last
_period: "5minute" | "hour" | "day" | "month" = "hour", _period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number, initValue: number,
maxDiff: number, maxDiff: number,

View File

@ -156,18 +156,6 @@ The `title ` option should not be used without a description.
*Documentation coming soon* *Documentation coming soon*
**Right to left**
<ha-alert alert-type="success" rtl>
This is an info alert — check it out!
</ha-alert>
```html
<ha-alert alert-type="success" rtl>
This is an info alert — check it out!
</ha-alert>
```
### API ### API
**Properties/Attributes** **Properties/Attributes**

View File

@ -115,8 +115,8 @@ export class DemoHaBarSwitch extends LitElement {
font-weight: 600; font-weight: 600;
} }
.custom { .custom {
--switch-bar-on-color: rgb(var(--rgb-green-color)); --switch-bar-on-color: var(--green-color);
--switch-bar-off-color: rgb(var(--rgb-red-color)); --switch-bar-off-color: var(--red-color);
--switch-bar-thickness: 100px; --switch-bar-thickness: 100px;
--switch-bar-border-radius: 24px; --switch-bar-border-radius: 24px;
--switch-bar-padding: 6px; --switch-bar-padding: 6px;

View File

@ -99,16 +99,19 @@ const AREAS = [
area_id: "backyard", area_id: "backyard",
name: "Backyard", name: "Backyard",
picture: null, picture: null,
aliases: [],
}, },
{ {
area_id: "bedroom", area_id: "bedroom",
name: "Bedroom", name: "Bedroom",
picture: null, picture: null,
aliases: [],
}, },
{ {
area_id: "livingroom", area_id: "livingroom",
name: "Livingroom", name: "Livingroom",
picture: null, picture: null,
aliases: [],
}, },
]; ];

View File

@ -95,16 +95,19 @@ const AREAS = [
area_id: "backyard", area_id: "backyard",
name: "Backyard", name: "Backyard",
picture: null, picture: null,
aliases: [],
}, },
{ {
area_id: "bedroom", area_id: "bedroom",
name: "Bedroom", name: "Bedroom",
picture: null, picture: null,
aliases: [],
}, },
{ {
area_id: "livingroom", area_id: "livingroom",
name: "Livingroom", name: "Livingroom",
picture: null, picture: null,
aliases: [],
}, },
]; ];

View File

@ -0,0 +1,3 @@
---
title: Tile Card
---

View File

@ -0,0 +1,173 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, query } from "lit/decorators";
import { CoverEntityFeature } from "../../../../src/data/cover";
import { LightColorMode } from "../../../../src/data/light";
import { VacuumEntityFeature } from "../../../../src/data/vacuum";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
const ENTITIES = [
getEntity("switch", "tv_outlet", "on", {
friendly_name: "TV outlet",
device_class: "outlet",
}),
getEntity("light", "bed_light", "on", {
friendly_name: "Bed Light",
supported_color_modes: [LightColorMode.HS],
}),
getEntity("light", "unavailable", "unavailable", {
friendly_name: "Unavailable entity",
}),
getEntity("climate", "thermostat", "heat", {
current_temperature: 73,
min_temp: 45,
max_temp: 95,
temperature: 80,
hvac_modes: ["heat", "cool", "auto", "off"],
friendly_name: "Thermostat",
hvac_action: "heating",
}),
getEntity("person", "paulus", "home", {
friendly_name: "Paulus",
}),
getEntity("vacuum", "first_floor_vacuum", "docked", {
friendly_name: "First floor vacuum",
supported_features:
VacuumEntityFeature.START +
VacuumEntityFeature.STOP +
VacuumEntityFeature.RETURN_HOME,
}),
getEntity("cover", "kitchen_shutter", "open", {
friendly_name: "Kitchen shutter",
device_class: "shutter",
supported_features:
CoverEntityFeature.CLOSE +
CoverEntityFeature.OPEN +
CoverEntityFeature.STOP,
}),
getEntity("cover", "pergola_roof", "open", {
friendly_name: "Pergola Roof",
supported_features:
CoverEntityFeature.CLOSE_TILT +
CoverEntityFeature.OPEN_TILT +
CoverEntityFeature.STOP_TILT,
}),
];
const CONFIGS = [
{
heading: "Basic example",
config: `
- type: tile
entity: switch.tv_outlet
`,
},
{
heading: "Vertical example",
config: `
- type: tile
entity: switch.tv_outlet
vertical: true
`,
},
{
heading: "Custom color",
config: `
- type: tile
entity: switch.tv_outlet
color: pink
`,
},
{
heading: "Unknown entity",
config: `
- type: tile
entity: light.unknown
`,
},
{
heading: "Unavailable entity",
config: `
- type: tile
entity: light.unavailable
`,
},
{
heading: "Climate",
config: `
- type: tile
entity: climate.thermostat
`,
},
{
heading: "Person",
config: `
- type: tile
entity: person.paulus
`,
},
{
heading: "Light brightness feature",
config: `
- type: tile
entity: light.bed_light
features:
- type: "light-brightness"
`,
},
{
heading: "Vacuum commands feature",
config: `
- type: tile
entity: vacuum.first_floor_vacuum
features:
- type: "vacuum-commands"
commands:
- start_pause
- stop
- return_home
`,
},
{
heading: "Cover open close feature",
config: `
- type: tile
entity: cover.kitchen_shutter
features:
- type: "cover-open-close"
`,
},
{
heading: "Cover tilt feature",
config: `
- type: tile
entity: cover.pergola_roof
features:
- type: "cover-tilt"
`,
},
];
@customElement("demo-lovelace-tile-card")
class DemoTile extends LitElement {
@query("#demos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-lovelace-tile-card": DemoTile;
}
}

View File

@ -104,16 +104,17 @@ const ENTITIES: HassEntity[] = [
createEntity("alarm_control_panel.disarming", "disarming"), createEntity("alarm_control_panel.disarming", "disarming"),
createEntity("alarm_control_panel.triggered", "triggered"), createEntity("alarm_control_panel.triggered", "triggered"),
// Alert // Alert
createEntity("alert.idle", "idle"),
createEntity("alert.off", "off"), createEntity("alert.off", "off"),
createEntity("alert.on", "on"), createEntity("alert.on", "on"),
createEntity("alert.idle", "idle"),
// Automation // Automation
createEntity("automation.off", "off"), createEntity("automation.off", "off"),
createEntity("automation.on", "on"), createEntity("automation.on", "on"),
// Binary Sensor // Binary Sensor
...BINARY_SENSOR_DEVICE_CLASSES.map((dc) => ...BINARY_SENSOR_DEVICE_CLASSES.map((dc) => [
createEntity(`binary_sensor.${dc}`, "on", dc) createEntity(`binary_sensor.${dc}`, "off", dc),
), createEntity(`binary_sensor.${dc}`, "on", dc),
]).reduce((arr, item) => [...arr, ...item], []),
// Button // Button
createEntity("button.restart", "unknown", "restart"), createEntity("button.restart", "unknown", "restart"),
createEntity("button.update", "unknown", "update"), createEntity("button.update", "unknown", "update"),
@ -142,6 +143,9 @@ const ENTITIES: HassEntity[] = [
createEntity("climate.auto_dry", "auto", undefined, { createEntity("climate.auto_dry", "auto", undefined, {
hvac_action: "drying", hvac_action: "drying",
}), }),
createEntity("climate.auto_fan", "auto", undefined, {
hvac_action: "fan",
}),
// Cover // Cover
createEntity("cover.closing", "closing"), createEntity("cover.closing", "closing"),
createEntity("cover.closed", "closed"), createEntity("cover.closed", "closed"),
@ -180,8 +184,8 @@ const ENTITIES: HassEntity[] = [
createEntity("light.off", "off"), createEntity("light.off", "off"),
createEntity("light.on", "on"), createEntity("light.on", "on"),
// Locks // Locks
createEntity("lock.unlocked", "unlocked"),
createEntity("lock.locked", "locked"), createEntity("lock.locked", "locked"),
createEntity("lock.unlocked", "unlocked"),
createEntity("lock.locking", "locking"), createEntity("lock.locking", "locking"),
createEntity("lock.unlocking", "unlocking"), createEntity("lock.unlocking", "unlocking"),
createEntity("lock.jammed", "jammed"), createEntity("lock.jammed", "jammed"),
@ -205,17 +209,24 @@ const ENTITIES: HassEntity[] = [
createEntity("media_player.speaker_playing", "playing", "speaker"), createEntity("media_player.speaker_playing", "playing", "speaker"),
createEntity("media_player.speaker_paused", "paused", "speaker"), createEntity("media_player.speaker_paused", "paused", "speaker"),
createEntity("media_player.speaker_standby", "standby", "speaker"), createEntity("media_player.speaker_standby", "standby", "speaker"),
// Plant
createEntity("plant.ok", "ok"),
createEntity("plant.problem", "problem"),
// Remote // Remote
createEntity("remote.off", "off"), createEntity("remote.off", "off"),
createEntity("remote.on", "on"), createEntity("remote.on", "on"),
// Schedule
createEntity("schedule.off", "off"),
createEntity("schedule.on", "on"),
// Script // Script
createEntity("script.off", "off"), createEntity("script.off", "off"),
createEntity("script.on", "on"), createEntity("script.on", "on"),
// Sensor // Sensor
...SENSOR_DEVICE_CLASSES.map((dc) => createEntity(`sensor.${dc}`, "10", dc)), ...SENSOR_DEVICE_CLASSES.map((dc) => createEntity(`sensor.${dc}`, "10", dc)),
// Battery sensor // Battery sensor
...[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map((value) => ...[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, "unknown", "not_valid"].map(
createEntity(`sensor.battery_${value}`, value.toString(), "battery") (value) =>
createEntity(`sensor.battery_${value}`, value.toString(), "battery")
), ),
// Siren // Siren
createEntity("siren.off", "off"), createEntity("siren.off", "off"),

View File

@ -197,6 +197,7 @@ const createEntityRegistryEntries = (
platform: "updater", platform: "updater",
has_entity_name: false, has_entity_name: false,
unique_id: "updater", unique_id: "updater",
options: null,
}, },
]; ];

View File

@ -15,7 +15,7 @@ class SupervisorFormfieldLabel extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
${this.imageUrl ${this.imageUrl
? html`<img loading="lazy" .src=${this.imageUrl} class="icon" />` ? html`<img loading="lazy" alt="" src=${this.imageUrl} class="icon" />`
: this.iconPath : this.iconPath
? html`<ha-svg-icon .path=${this.iconPath} class="icon"></ha-svg-icon>` ? html`<ha-svg-icon .path=${this.iconPath} class="icon"></ha-svg-icon>`
: ""} : ""}

View File

@ -138,7 +138,10 @@ export class DialogHassioNetwork
)} )}
${this._interface?.type === "wireless" ${this._interface?.type === "wireless"
? html` ? html`
<ha-expansion-panel header="Wi-Fi" outlined> <ha-expansion-panel
.header=${this.supervisor.localize("dialog.network.wifi")}
outlined
>
${this._interface?.wifi?.ssid ${this._interface?.wifi?.ssid
? html`<p> ? html`<p>
${this.supervisor.localize( ${this.supervisor.localize(
@ -177,7 +180,11 @@ export class DialogHassioNetwork
> >
<span>${ap.ssid}</span> <span>${ap.ssid}</span>
<span slot="secondary"> <span slot="secondary">
${ap.mac} - Strength: ${ap.signal} ${ap.mac} -
${this.supervisor.localize(
"dialog.network.signal_strength"
)}:
${ap.signal}
</span> </span>
</mwc-list-item> </mwc-list-item>
` `
@ -241,7 +248,9 @@ export class DialogHassioNetwork
class="flex-auto" class="flex-auto"
type="password" type="password"
id="psk" id="psk"
label="Password" .label=${this.supervisor.localize(
"dialog.network.wifi_password"
)}
version="wifi" version="wifi"
@value-changed=${this @value-changed=${this
._handleInputValueChangedWifi} ._handleInputValueChangedWifi}

View File

@ -60,8 +60,8 @@ class HassioIngressView extends LitElement {
} }
const iframe = html`<iframe const iframe = html`<iframe
.title=${this._addon.name} title=${this._addon.name}
.src=${this._addon.ingress_url!} src=${this._addon.ingress_url!}
> >
</iframe>`; </iframe>`;

View File

@ -5,5 +5,5 @@ module.exports = {
'printf "%s\n" "Translation files should not be added or modified here. Instead, make the necessary modifications in src/translations/en.json. Other languages are managed externally. Please see https://developers.home-assistant.io/docs/translations/ for details." ' + 'printf "%s\n" "Translation files should not be added or modified here. Instead, make the necessary modifications in src/translations/en.json. Other languages are managed externally. Please see https://developers.home-assistant.io/docs/translations/ for details." ' +
files.join(" ") + files.join(" ") +
" >&2 && exit 1", " >&2 && exit 1",
"/yarn.lock": () => "yarn dedupe", "yarn.lock": () => "yarn dedupe",
}; };

View File

@ -24,53 +24,52 @@
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)", "author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "^6.0.0", "@braintree/sanitize-url": "^6.0.2",
"@codemirror/autocomplete": "^6.4.0", "@codemirror/autocomplete": "^6.4.0",
"@codemirror/commands": "^6.1.3", "@codemirror/commands": "^6.2.0",
"@codemirror/language": "^6.3.2", "@codemirror/language": "^6.4.0",
"@codemirror/legacy-modes": "^6.3.1", "@codemirror/legacy-modes": "^6.3.1",
"@codemirror/search": "^6.2.3", "@codemirror/search": "^6.2.3",
"@codemirror/state": "^6.2.0", "@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.7.1", "@codemirror/view": "^6.8.1",
"@formatjs/intl-datetimeformat": "^4.2.5", "@formatjs/intl-datetimeformat": "^6.4.3",
"@formatjs/intl-getcanonicallocales": "^1.8.0", "@formatjs/intl-getcanonicallocales": "^2.0.5",
"@formatjs/intl-locale": "^2.4.40", "@formatjs/intl-locale": "^3.0.11",
"@formatjs/intl-numberformat": "^7.2.5", "@formatjs/intl-numberformat": "^8.3.3",
"@formatjs/intl-pluralrules": "^4.1.5", "@formatjs/intl-pluralrules": "^5.1.8",
"@formatjs/intl-relativetimeformat": "^9.3.2", "@formatjs/intl-relativetimeformat": "^11.1.8",
"@fullcalendar/common": "5.9.0", "@fullcalendar/core": "^6.1.4",
"@fullcalendar/core": "5.9.0", "@fullcalendar/daygrid": "^6.1.4",
"@fullcalendar/daygrid": "5.9.0", "@fullcalendar/interaction": "^6.1.4",
"@fullcalendar/interaction": "5.9.0", "@fullcalendar/list": "^6.1.4",
"@fullcalendar/list": "5.9.0", "@fullcalendar/timegrid": "^6.1.4",
"@fullcalendar/timegrid": "5.9.0",
"@lezer/highlight": "^1.1.3", "@lezer/highlight": "^1.1.3",
"@lit-labs/motion": "^1.0.2", "@lit-labs/motion": "^1.0.3",
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch", "@lit-labs/virtualizer": "^1.0.1",
"@material/chips": "14.0.0-canary.261f2db59.0", "@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "14.0.0-canary.261f2db59.0", "@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-button": "0.25.3", "@material/mwc-button": "^0.27.0",
"@material/mwc-checkbox": "0.25.3", "@material/mwc-checkbox": "^0.27.0",
"@material/mwc-circular-progress": "0.25.3", "@material/mwc-circular-progress": "^0.27.0",
"@material/mwc-dialog": "0.25.3", "@material/mwc-dialog": "^0.27.0",
"@material/mwc-drawer": "^0.25.3", "@material/mwc-drawer": "^0.27.0",
"@material/mwc-fab": "0.25.3", "@material/mwc-fab": "^0.27.0",
"@material/mwc-formfield": "0.25.3", "@material/mwc-formfield": "^0.27.0",
"@material/mwc-icon-button": "patch:@material/mwc-icon-button@0.25.3#./.yarn/patches/@material/mwc-icon-button/remove-icon.patch", "@material/mwc-icon-button": "^0.27.0",
"@material/mwc-linear-progress": "0.25.3", "@material/mwc-linear-progress": "^0.27.0",
"@material/mwc-list": "^0.25.3", "@material/mwc-list": "^0.27.0",
"@material/mwc-menu": "0.25.3", "@material/mwc-menu": "^0.27.0",
"@material/mwc-radio": "0.25.3", "@material/mwc-radio": "^0.27.0",
"@material/mwc-ripple": "0.25.3", "@material/mwc-ripple": "^0.27.0",
"@material/mwc-select": "0.25.3", "@material/mwc-select": "^0.27.0",
"@material/mwc-slider": "0.25.3", "@material/mwc-slider": "^0.27.0",
"@material/mwc-switch": "0.25.3", "@material/mwc-switch": "^0.27.0",
"@material/mwc-tab": "0.25.3", "@material/mwc-tab": "^0.27.0",
"@material/mwc-tab-bar": "0.25.3", "@material/mwc-tab-bar": "^0.27.0",
"@material/mwc-textarea": "^0.25.3", "@material/mwc-textarea": "^0.27.0",
"@material/mwc-textfield": "0.25.3", "@material/mwc-textfield": "^0.27.0",
"@material/mwc-top-app-bar-fixed": "^0.25.3", "@material/mwc-top-app-bar-fixed": "^0.27.0",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0", "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@mdi/js": "7.1.96", "@mdi/js": "7.1.96",
"@mdi/svg": "7.1.96", "@mdi/svg": "7.1.96",
"@polymer/app-layout": "^3.1.0", "@polymer/app-layout": "^3.1.0",
@ -87,52 +86,53 @@
"@polymer/paper-toast": "^3.0.1", "@polymer/paper-toast": "^3.0.1",
"@polymer/paper-tooltip": "^3.0.1", "@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.4.1", "@polymer/polymer": "3.4.1",
"@thomasloven/round-slider": "0.5.4", "@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "^23.2.9", "@vaadin/combo-box": "^23.3.6",
"@vaadin/vaadin-themable-mixin": "^23.2.9", "@vaadin/vaadin-themable-mixin": "^23.3.6",
"@vibrant/color": "^3.2.1-alpha.1", "@vibrant/color": "^3.2.1-alpha.1",
"@vibrant/core": "^3.2.1-alpha.1", "@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
"@vue/web-component-wrapper": "^1.3.0", "@vue/web-component-wrapper": "^1.3.0",
"@webcomponents/scoped-custom-element-registry": "^0.0.5", "@webcomponents/scoped-custom-element-registry": "^0.0.8",
"@webcomponents/webcomponentsjs": "^2.2.10", "@webcomponents/webcomponentsjs": "^2.7.0",
"app-datepicker": "^5.1.0", "app-datepicker": "^5.1.0",
"chart.js": "^3.3.2", "chart.js": "^3.3.2",
"comlink": "^4.3.1", "comlink": "^4.4.1",
"core-js": "^3.15.2", "core-js": "^3.27.2",
"cropperjs": "^1.5.12", "cropperjs": "^1.5.13",
"date-fns": "^2.23.0", "date-fns": "^2.29.3",
"date-fns-tz": "^1.3.7", "date-fns-tz": "^2.0.0",
"deep-clone-simple": "^1.1.1", "deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1", "deep-freeze": "^0.0.1",
"fuse.js": "^6.0.0", "fuse.js": "^6.6.2",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"hls.js": "^1.2.5", "hls.js": "^1.3.3",
"home-assistant-js-websocket": "^8.0.1", "home-assistant-js-websocket": "^8.0.1",
"idb-keyval": "^5.1.3", "idb-keyval": "^6.2.0",
"intl-messageformat": "^9.9.1", "intl-messageformat": "^10.3.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"leaflet": "^1.7.1", "leaflet": "^1.9.3",
"leaflet-draw": "^1.0.4", "leaflet-draw": "^1.0.4",
"lit": "^2.1.2", "lit": "^2.6.1",
"marked": "^4.0.12", "marked": "^4.2.12",
"memoize-one": "^5.2.1", "memoize-one": "^6.0.0",
"node-vibrant": "3.2.1-alpha.1", "node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "^0.3.2", "proxy-polyfill": "^0.3.2",
"punycode": "^2.1.1", "punycode": "^2.3.0",
"qr-scanner": "^1.3.0", "qr-scanner": "^1.4.2",
"qrcode": "^1.4.4", "qrcode": "^1.5.1",
"regenerator-runtime": "^0.13.8", "regenerator-runtime": "^0.13.11",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"rrule": "^2.7.1", "rrule": "^2.7.2",
"sortablejs": "^1.14.0", "sortablejs": "^1.15.0",
"superstruct": "^0.15.2", "superstruct": "^1.0.3",
"tinykeys": "^1.1.3", "tinykeys": "^1.4.0",
"tsparticles": "^1.34.0", "tsparticles-engine": "^2.9.3",
"unfetch": "^4.1.0", "tsparticles-preset-links": "^2.8.0",
"vis-data": "^7.1.2", "unfetch": "^5.0.0",
"vis-data": "^7.1.4",
"vis-network": "^8.5.4", "vis-network": "^8.5.4",
"vue": "^2.6.12", "vue": "^2.6.12",
"vue2-daterange-picker": "^0.5.1", "vue2-daterange-picker": "^0.5.1",
@ -143,119 +143,115 @@
"workbox-precaching": "^6.5.4", "workbox-precaching": "^6.5.4",
"workbox-routing": "^6.5.4", "workbox-routing": "^6.5.4",
"workbox-strategies": "^6.5.4", "workbox-strategies": "^6.5.4",
"xss": "^1.0.9" "xss": "^1.0.14"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.2", "@babel/core": "^7.20.12",
"@babel/plugin-external-helpers": "^7.18.6", "@babel/plugin-external-helpers": "^7.18.6",
"@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.20.2", "@babel/plugin-proposal-decorators": "^7.20.13",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
"@babel/plugin-proposal-object-rest-spread": "^7.20.2", "@babel/plugin-proposal-object-rest-spread": "^7.20.7",
"@babel/plugin-proposal-optional-chaining": "^7.18.9", "@babel/plugin-proposal-optional-chaining": "^7.20.7",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5",
"@babel/preset-env": "^7.20.2", "@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.18.6", "@babel/preset-typescript": "^7.18.6",
"@koa/cors": "^3.1.0", "@koa/cors": "^4.0.0",
"@octokit/auth-oauth-device": "^4.0.2", "@octokit/auth-oauth-device": "^4.0.4",
"@octokit/rest": "^19.0.4", "@octokit/rest": "^19.0.7",
"@open-wc/dev-server-hmr": "^0.0.2", "@open-wc/dev-server-hmr": "^0.1.3",
"@rollup/plugin-babel": "^5.2.1", "@rollup/plugin-babel": "^5.2.1",
"@rollup/plugin-commonjs": "^11.1.0", "@rollup/plugin-commonjs": "^11.1.0",
"@rollup/plugin-json": "^4.0.3", "@rollup/plugin-json": "^4.0.3",
"@rollup/plugin-node-resolve": "^7.1.3", "@rollup/plugin-node-resolve": "^7.1.3",
"@rollup/plugin-replace": "^2.3.2", "@rollup/plugin-replace": "^2.3.2",
"@types/chromecast-caf-receiver": "5.0.12", "@types/chromecast-caf-receiver": "5.0.12",
"@types/chromecast-caf-sender": "^1.0.3", "@types/chromecast-caf-sender": "^1.0.5",
"@types/glob": "^7", "@types/glob": "^8",
"@types/hammerjs": "^2.0.41", "@types/hammerjs": "^2.0.41",
"@types/js-yaml": "^4", "@types/js-yaml": "^4",
"@types/leaflet": "^1", "@types/leaflet": "^1",
"@types/leaflet-draw": "^1", "@types/leaflet-draw": "^1",
"@types/marked": "^4", "@types/marked": "^4",
"@types/mocha": "^8", "@types/mocha": "^8",
"@types/qrcode": "^1.4.2", "@types/qrcode": "^1.5.0",
"@types/sortablejs": "^1", "@types/sortablejs": "^1",
"@types/tar": "^6", "@types/tar": "^6",
"@types/webspeechapi": "^0.0.29", "@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^5.46.1", "@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.44.0", "@typescript-eslint/parser": "^5.52.0",
"@web/dev-server": "^0.0.24", "@web/dev-server": "^0.0.24",
"@web/dev-server-rollup": "^0.2.11", "@web/dev-server-rollup": "^0.2.11",
"babel-loader": "^9.1.0", "babel-loader": "^9.1.2",
"chai": "^4.3.4", "chai": "^4.3.7",
"del": "^4.0.0", "del": "^7.0.0",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.2.1", "eslint-config-airbnb-base": "^14.2.1",
"eslint-config-airbnb-typescript": "^14.0.0", "eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.6.0",
"eslint-import-resolver-webpack": "^0.13.1", "eslint-import-resolver-webpack": "^0.13.2",
"eslint-plugin-disable": "^2.0.1", "eslint-plugin-disable": "^2.0.3",
"eslint-plugin-import": "^2.24.2", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-lit": "^1.6.1", "eslint-plugin-lit": "^1.8.2",
"eslint-plugin-lit-a11y": "^2.3.0",
"eslint-plugin-unused-imports": "^1.1.5", "eslint-plugin-unused-imports": "^1.1.5",
"eslint-plugin-wc": "^1.3.2", "eslint-plugin-wc": "^1.4.0",
"fancy-log": "^2.0.0", "fancy-log": "^2.0.0",
"fs-extra": "^11.1.0", "fs-extra": "^11.1.0",
"glob": "^7.2.0", "glob": "^8.1.0",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-flatmap": "^1.0.2", "gulp-flatmap": "^1.0.2",
"gulp-json-transform": "^0.4.6", "gulp-json-transform": "^0.4.8",
"gulp-merge-json": "^1.3.1", "gulp-merge-json": "^2.1.2",
"gulp-rename": "^2.0.0", "gulp-rename": "^2.0.0",
"gulp-zopfli-green": "^3.0.1", "gulp-zopfli-green": "^6.0.1",
"html-minifier": "^4.0.0", "html-minifier": "^4.0.0",
"husky": "^8.0.1", "husky": "^8.0.3",
"instant-mocha": "^1.3.1", "instant-mocha": "^1.5.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lint-staged": "^13.0.3", "lint-staged": "^13.1.1",
"lit-analyzer": "^1.2.1", "lit-analyzer": "^1.2.1",
"lodash.template": "^4.5.0", "lodash.template": "^4.5.0",
"magic-string": "^0.25.7", "magic-string": "^0.25.7",
"map-stream": "^0.0.7", "map-stream": "^0.0.7",
"merge-stream": "^1.0.1", "merge-stream": "^2.0.0",
"mocha": "^8.4.0", "mocha": "^8.4.0",
"object-hash": "^2.0.3", "object-hash": "^3.0.0",
"open": "^8.4.0", "open": "^8.4.0",
"pinst": "^3.0.0", "pinst": "^3.0.0",
"prettier": "^2.8.1", "prettier": "^2.8.4",
"require-dir": "^1.2.0", "require-dir": "^1.2.0",
"rollup": "^2.8.2", "rollup": "^2.8.2",
"rollup-plugin-string": "^3.0.0", "rollup-plugin-string": "^3.0.0",
"rollup-plugin-terser": "^5.3.0", "rollup-plugin-terser": "^5.3.0",
"rollup-plugin-visualizer": "^4.0.4", "rollup-plugin-visualizer": "^5.9.0",
"serve": "^11.3.2", "serve": "^11.3.2",
"sinon": "^11.0.0", "sinon": "^15.0.1",
"source-map-url": "^0.4.0", "source-map-url": "^0.4.1",
"systemjs": "^6.3.2", "systemjs": "^6.13.0",
"tar": "^6.1.11", "tar": "^6.1.13",
"terser-webpack-plugin": "^5.2.4", "terser-webpack-plugin": "^5.3.6",
"ts-lit-plugin": "^1.2.1", "ts-lit-plugin": "^1.2.1",
"typescript": "^4.9.4", "typescript": "^4.9.5",
"vinyl-buffer": "^1.0.1", "vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0", "vinyl-source-stream": "^2.0.0",
"webpack": "^5.55.1", "webpack": "^5.55.1",
"webpack-cli": "^4.8.0", "webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.3.0", "webpack-dev-server": "^4.11.1",
"webpack-manifest-plugin": "^4.0.2", "webpack-manifest-plugin": "^5.0.0",
"webpackbar": "^5.0.0-3", "webpackbar": "^5.0.2",
"workbox-build": "^6.5.4" "workbox-build": "^6.5.4"
}, },
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch", "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": { "resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch", "@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch"
"@webcomponents/webcomponentsjs": "^2.2.10",
"lit": "^2.1.2",
"lit-html": "2.1.2",
"lit-element": "3.1.2",
"@lit/reactive-element": "1.2.1"
}, },
"main": "src/home-assistant.js", "main": "src/home-assistant.js",
"prettier": { "prettier": {
"trailingComma": "es5", "trailingComma": "es5",
"arrowParens": "always" "arrowParens": "always"
}, },
"packageManager": "yarn@3.2.3" "packageManager": "yarn@3.3.1"
} }

View File

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

View File

@ -19,7 +19,9 @@ export const THEME_COLORS = new Set([
"orange", "orange",
"deep-orange", "deep-orange",
"brown", "brown",
"light-grey",
"grey", "grey",
"dark-grey",
"blue-grey", "blue-grey",
"black", "black",
"white", "white",
@ -27,7 +29,7 @@ export const THEME_COLORS = new Set([
export function computeCssColor(color: string): string { export function computeCssColor(color: string): string {
if (THEME_COLORS.has(color)) { if (THEME_COLORS.has(color)) {
return `rgb(var(--rgb-${color}-color))`; return `var(--${color}-color)`;
} }
return color; return color;
} }

View File

@ -22,3 +22,11 @@ export const atLeastVersion = (
Number(haPatch) >= patch) Number(haPatch) >= patch)
); );
}; };
export const isDevVersion = (version: string): boolean => {
if (__DEMO__) {
return false;
}
return version.includes("dev");
};

View File

@ -1,6 +1,12 @@
import { getWeekStartByLocale } from "weekstart"; import { getWeekStartByLocale } from "weekstart";
import { FrontendLocaleData, FirstWeekday } from "../../data/translation"; import { FrontendLocaleData, FirstWeekday } from "../../data/translation";
import { polyfillsLoaded } from "../translations/localize";
if (__BUILD__ === "latest" && polyfillsLoaded) {
await polyfillsLoaded;
}
export const weekdays = [ export const weekdays = [
"sunday", "sunday",
"monday", "monday",

View File

@ -18,7 +18,7 @@ export const relativeTime = (
to?: Date, to?: Date,
includeTense = true includeTense = true
): string => { ): string => {
const diff = selectUnit(from, to); const diff = selectUnit(from, to, locale);
if (includeTense) { if (includeTense) {
return formatRelTimeMem(locale).format(diff.value, diff.unit); return formatRelTimeMem(locale).format(diff.value, diff.unit);
} }

View File

@ -39,5 +39,5 @@ export default function scrollToTarget(element, target) {
); );
requestAnimationFrame(updateFrame.bind(element)); requestAnimationFrame(updateFrame.bind(element));
} }
}.call(element)); }).call(element);
} }

View File

@ -11,8 +11,7 @@ export const setupLeafletMap = async (
throw new Error("Cannot setup Leaflet map on disconnected element"); throw new Error("Cannot setup Leaflet map on disconnected element");
} }
// eslint-disable-next-line // eslint-disable-next-line
const Leaflet = ((await import("leaflet")) as any) const Leaflet = (await import("leaflet")).default as LeafletModuleType;
.default as LeafletModuleType;
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/"; Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
const map = Leaflet.map(mapElement); const map = Leaflet.map(mapElement);

View File

@ -1,21 +0,0 @@
export const alarmControlPanelColor = (state?: string): string | undefined => {
switch (state) {
case "armed_away":
case "armed_vacation":
case "armed_home":
case "armed_night":
case "armed_custom_bypass":
return "alarm-armed";
case "pending":
return "alarm-pending";
case "arming":
case "disarming":
return "alarm-arming";
case "triggered":
return "alarm-triggered";
case "disarmed":
return "alarm-disarmed";
default:
return undefined;
}
};

View File

@ -1,10 +0,0 @@
export const alertColor = (state?: string): string | undefined => {
switch (state) {
case "on":
return "alert";
case "off":
return "alert-off";
default:
return undefined;
}
};

View File

@ -1,13 +1,15 @@
export const batteryStateColor = (state: string) => { export const batteryStateColorProperty = (
state: string
): string | undefined => {
const value = Number(state); const value = Number(state);
if (isNaN(value)) { if (isNaN(value)) {
return undefined; return undefined;
} }
if (value >= 70) { if (value >= 70) {
return "sensor-battery-high"; return "--state-sensor-battery-high-color";
} }
if (value >= 30) { if (value >= 30) {
return "sensor-battery-medium"; return "--state-sensor-battery-medium-color";
} }
return "sensor-battery-low"; return "--state-sensor-battery-low-color";
}; };

View File

@ -1,29 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { stateActive } from "../state_active";
const ALERTING_DEVICE_CLASSES = new Set([
"battery",
"carbon_monoxide",
"gas",
"heat",
"lock",
"moisture",
"problem",
"safety",
"smoke",
"tamper",
]);
export const binarySensorColor = (
stateObj: HassEntity,
state: string
): string | undefined => {
const deviceClass = stateObj?.attributes.device_class;
if (!stateActive(stateObj, state)) {
return undefined;
}
return deviceClass && ALERTING_DEVICE_CLASSES.has(deviceClass)
? "binary-sensor-alerting"
: "binary-sensor";
};

View File

@ -1,29 +0,0 @@
import { HvacAction } from "../../../data/climate";
export const CLIMATE_HVAC_ACTION_COLORS: Record<HvacAction, string> = {
cooling: "var(--rgb-state-climate-cool-color)",
drying: "var(--rgb-state-climate-dry-color)",
fan: "var(--rgb-state-climate-fan-only-color)",
heating: "var(--rgb-state-climate-heat-color)",
idle: "var(--rgb-state-climate-idle-color)",
off: "var(--rgb-state-climate-off-color)",
};
export const climateColor = (state: string): string | undefined => {
switch (state) {
case "auto":
return "climate-auto";
case "cool":
return "climate-cool";
case "dry":
return "climate-dry";
case "fan_only":
return "climate-fan-only";
case "heat":
return "climate-heat";
case "heat_cool":
return "climate-heat-cool";
default:
return undefined;
}
};

View File

@ -1,15 +0,0 @@
export const lockColor = (state?: string): string | undefined => {
switch (state) {
case "unlocked":
return "lock-unlocked";
case "locked":
return "lock-locked";
case "jammed":
return "lock-jammed";
case "locking":
case "unlocking":
return "lock-pending";
default:
return undefined;
}
};

View File

@ -1,10 +0,0 @@
export const personColor = (state: string): string | undefined => {
switch (state) {
case "home":
return "person-home";
case "not_home":
return "person-not-home";
default:
return "person-zone";
}
};

View File

@ -1,15 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { batteryStateColor } from "./battery_color";
export const sensorColor = (
stateObj: HassEntity,
state: string
): string | undefined => {
const deviceClass = stateObj?.attributes.device_class;
if (deviceClass === "battery") {
return batteryStateColor(state);
}
return undefined;
};

View File

@ -1,15 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { UpdateEntity, updateIsInstalling } from "../../../data/update";
import { stateActive } from "../state_active";
export const updateColor = (
stateObj: HassEntity,
state: string
): string | undefined => {
if (!stateActive(stateObj, state)) {
return undefined;
}
return updateIsInstalling(stateObj as UpdateEntity)
? "update-installing"
: "update";
};

View File

@ -49,6 +49,8 @@ export const computeStateDisplayFromEntityAttributes = (
return localize(`state.default.${state}`); return localize(`state.default.${state}`);
} }
const entity = entities[entityId] as EntityRegistryEntry | undefined;
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
if (isNumericFromAttributes(attributes)) { if (isNumericFromAttributes(attributes)) {
// state is duration // state is duration
@ -82,7 +84,7 @@ export const computeStateDisplayFromEntityAttributes = (
return `${formatNumber( return `${formatNumber(
state, state,
locale, locale,
getNumberFormatOptions({ state, attributes } as HassEntity) getNumberFormatOptions({ state, attributes } as HassEntity, entity)
)}${unit}`; )}${unit}`;
} }
@ -160,7 +162,7 @@ export const computeStateDisplayFromEntityAttributes = (
return formatNumber( return formatNumber(
state, state,
locale, locale,
getNumberFormatOptions({ state, attributes } as HassEntity) getNumberFormatOptions({ state, attributes } as HassEntity, entity)
); );
} }
@ -199,8 +201,6 @@ export const computeStateDisplayFromEntityAttributes = (
: localize("ui.card.update.up_to_date"); : localize("ui.card.update.up_to_date");
} }
const entity = entities[entityId] as EntityRegistryEntry | undefined;
return ( return (
(entity?.translation_key && (entity?.translation_key &&
localize( localize(

View File

@ -3,6 +3,8 @@ import {
mdiAccountArrowRight, mdiAccountArrowRight,
mdiAirHumidifier, mdiAirHumidifier,
mdiAirHumidifierOff, mdiAirHumidifierOff,
mdiAudioVideo,
mdiAudioVideoOff,
mdiBluetooth, mdiBluetooth,
mdiBluetoothConnect, mdiBluetoothConnect,
mdiCalendar, mdiCalendar,
@ -25,8 +27,6 @@ import {
mdiPackageUp, mdiPackageUp,
mdiPowerPlug, mdiPowerPlug,
mdiPowerPlugOff, mdiPowerPlugOff,
mdiAudioVideo,
mdiAudioVideoOff,
mdiRestart, mdiRestart,
mdiSpeaker, mdiSpeaker,
mdiSpeakerOff, mdiSpeakerOff,
@ -53,6 +53,7 @@ import { DEFAULT_DOMAIN_ICON, FIXED_DOMAIN_ICONS } from "../const";
import { alarmPanelIcon } from "./alarm_panel_icon"; import { alarmPanelIcon } from "./alarm_panel_icon";
import { binarySensorIcon } from "./binary_sensor_icon"; import { binarySensorIcon } from "./binary_sensor_icon";
import { coverIcon } from "./cover_icon"; import { coverIcon } from "./cover_icon";
import { numberIcon } from "./number_icon";
import { sensorIcon } from "./sensor_icon"; import { sensorIcon } from "./sensor_icon";
export const domainIcon = ( export const domainIcon = (
@ -108,7 +109,7 @@ export const domainIconWithoutDefault = (
return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount; return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount;
case "humidifier": case "humidifier":
return state && state === "off" ? mdiAirHumidifierOff : mdiAirHumidifier; return compareState === "off" ? mdiAirHumidifierOff : mdiAirHumidifier;
case "input_boolean": case "input_boolean":
return compareState === "on" return compareState === "on"
@ -180,6 +181,15 @@ export const domainIconWithoutDefault = (
} }
} }
case "number": {
const icon = numberIcon(stateObj);
if (icon) {
return icon;
}
break;
}
case "person": case "person":
return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount; return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount;

View File

@ -0,0 +1,13 @@
/** Return an icon representing a number state. */
import { HassEntity } from "home-assistant-js-websocket";
import { FIXED_DEVICE_CLASS_ICONS } from "../const";
export const numberIcon = (stateObj?: HassEntity): string | undefined => {
const dclass = stateObj?.attributes.device_class;
if (dclass && dclass in FIXED_DEVICE_CLASS_ICONS) {
return FIXED_DEVICE_CLASS_ICONS[dclass];
}
return undefined;
};

View File

@ -1,94 +1,102 @@
/** Return an color representing a state. */ /** Return an color representing a state. */
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE } from "../../data/entity"; import { UNAVAILABLE } from "../../data/entity";
import { alarmControlPanelColor } from "./color/alarm_control_panel_color"; import { computeCssVariable } from "../../resources/css-variables";
import { alertColor } from "./color/alert_color"; import { slugify } from "../string/slugify";
import { binarySensorColor } from "./color/binary_sensor_color"; import { batteryStateColorProperty } from "./color/battery_color";
import { climateColor } from "./color/climate_color";
import { lockColor } from "./color/lock_color";
import { personColor } from "./color/person_color";
import { sensorColor } from "./color/sensor_color";
import { updateColor } from "./color/update_color";
import { computeDomain } from "./compute_domain"; import { computeDomain } from "./compute_domain";
import { stateActive } from "./state_active"; import { stateActive } from "./state_active";
const STATIC_ACTIVE_COLORED_DOMAIN = new Set([ const STATE_COLORED_DOMAIN = new Set([
"alarm_control_panel",
"alert",
"automation", "automation",
"binary_sensor",
"calendar", "calendar",
"camera", "camera",
"climate",
"cover", "cover",
"device_tracker",
"fan", "fan",
"group", "group",
"humidifier", "humidifier",
"input_boolean", "input_boolean",
"light", "light",
"lock",
"media_player", "media_player",
"person",
"plant",
"remote", "remote",
"schedule",
"script", "script",
"siren", "siren",
"sun",
"switch", "switch",
"timer", "timer",
"update",
"vacuum", "vacuum",
]); ]);
export const stateColorCss = (stateObj: HassEntity, state?: string) => { export const stateColorCss = (stateObj: HassEntity, state?: string) => {
const compareState = state !== undefined ? state : stateObj?.state; const compareState = state !== undefined ? state : stateObj?.state;
if (compareState === UNAVAILABLE) { if (compareState === UNAVAILABLE) {
return `var(--rgb-state-unavailable-color)`; return `var(--state-unavailable-color)`;
} }
const domainColor = stateColor(stateObj, state); const properties = stateColorProperties(stateObj, state);
if (properties) {
if (domainColor) { return computeCssVariable(properties);
return `var(--rgb-state-${domainColor}-color)`;
}
if (!stateActive(stateObj, state)) {
return `var(--rgb-state-inactive-color)`;
} }
return undefined; return undefined;
}; };
export const stateColor = (stateObj: HassEntity, state?: string) => { export const domainStateColorProperties = (
stateObj: HassEntity,
state?: string
): string[] => {
const compareState = state !== undefined ? state : stateObj.state;
const domain = computeDomain(stateObj.entity_id);
const active = stateActive(stateObj, state);
const properties: string[] = [];
const stateKey = slugify(compareState, "_");
const activeKey = active ? "active" : "inactive";
const dc = stateObj.attributes.device_class;
if (dc) {
properties.push(`--state-${domain}-${dc}-${stateKey}-color`);
}
properties.push(
`--state-${domain}-${stateKey}-color`,
`--state-${domain}-${activeKey}-color`,
`--state-${activeKey}-color`
);
return properties;
};
export const stateColorProperties = (
stateObj: HassEntity,
state?: string
): string[] | undefined => {
const compareState = state !== undefined ? state : stateObj?.state; const compareState = state !== undefined ? state : stateObj?.state;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
const dc = stateObj.attributes.device_class;
if ( // Special rules for battery coloring
STATIC_ACTIVE_COLORED_DOMAIN.has(domain) && if (domain === "sensor" && dc === "battery") {
stateActive(stateObj, state) const property = batteryStateColorProperty(compareState);
) { if (property) {
return domain.replace("_", "-"); return [property];
}
} }
switch (domain) { if (STATE_COLORED_DOMAIN.has(domain)) {
case "alarm_control_panel": return domainStateColorProperties(stateObj, state);
return alarmControlPanelColor(compareState);
case "alert":
return alertColor(compareState);
case "binary_sensor":
return binarySensorColor(stateObj, compareState);
case "climate":
return climateColor(compareState);
case "lock":
return lockColor(compareState);
case "person":
case "device_tracker":
return personColor(compareState);
case "sensor":
return sensorColor(stateObj, compareState);
case "sun":
return compareState === "above_horizon" ? "sun-day" : "sun-night";
case "update":
return updateColor(stateObj, compareState);
} }
return undefined; return undefined;

View File

@ -4,12 +4,15 @@ import { domainToName } from "../../data/integration";
import { getIntegrationDescriptions } from "../../data/integrations"; import { getIntegrationDescriptions } from "../../data/integrations";
import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow"; import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import { showMatterAddDeviceDialog } from "../../panels/config/integrations/integration-panels/matter/show-dialog-add-matter-device";
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node"; import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url"; import { documentationUrl } from "../../util/documentation-url";
import { isComponentLoaded } from "../config/is_component_loaded"; import { isComponentLoaded } from "../config/is_component_loaded";
import { navigate } from "../navigate"; import { navigate } from "../navigate";
export const PROTOCOL_INTEGRATIONS = ["zha", "zwave_js", "matter"] as const;
export const protocolIntegrationPicked = async ( export const protocolIntegrationPicked = async (
element: HTMLElement, element: HTMLElement,
hass: HomeAssistant, hass: HomeAssistant,
@ -113,5 +116,43 @@ export const protocolIntegrationPicked = async (
} }
navigate("/config/zha/add"); navigate("/config/zha/add");
} else if (domain === "matter") {
const entries = await getConfigEntries(hass, {
domain,
});
if (!isComponentLoaded(hass, domain) || !entries.length) {
// If the component isn't loaded, ask them to load the integration first
showConfirmationDialog(element, {
title: hass.localize(
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee_title",
{ integration: "Matter" }
),
text: hass.localize(
"ui.panel.config.integrations.config_flow.missing_matter",
{
integration: "Matter",
brand: options?.brand || options?.domain || "Matter",
supported_hardware_link: html`<a
href=${documentationUrl(hass, "/integrations/matter")}
target="_blank"
rel="noreferrer"
>${hass.localize(
"ui.panel.config.integrations.config_flow.supported_hardware"
)}</a
>`,
}
),
confirmText: hass.localize(
"ui.panel.config.integrations.config_flow.proceed"
),
confirm: () => {
showConfigFlowDialog(element, {
startFlowHandler: "matter",
});
},
});
return;
}
showMatterAddDeviceDialog(element);
} }
}; };

View File

@ -2,6 +2,7 @@ import {
HassEntity, HassEntity,
HassEntityAttributeBase, HassEntityAttributeBase,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { FrontendLocaleData, NumberFormat } from "../../data/translation"; import { FrontendLocaleData, NumberFormat } from "../../data/translation";
import { round } from "./round"; import { round } from "./round";
@ -90,8 +91,18 @@ export const formatNumber = (
* @returns An `Intl.NumberFormatOptions` object with `maximumFractionDigits` set to 0, or `undefined` * @returns An `Intl.NumberFormatOptions` object with `maximumFractionDigits` set to 0, or `undefined`
*/ */
export const getNumberFormatOptions = ( export const getNumberFormatOptions = (
entityState: HassEntity entityState: HassEntity,
entity?: EntityRegistryEntry
): Intl.NumberFormatOptions | undefined => { ): Intl.NumberFormatOptions | undefined => {
const precision =
entity?.options?.sensor?.display_precision ??
entity?.options?.sensor?.suggested_display_precision;
if (precision != null) {
return {
maximumFractionDigits: precision,
minimumFractionDigits: precision,
};
}
if ( if (
Number.isInteger(Number(entityState.attributes?.step)) && Number.isInteger(Number(entityState.attributes?.step)) &&
Number.isInteger(Number(entityState.state)) Number.isInteger(Number(entityState.state))

View File

@ -22,6 +22,6 @@ export const iconColorCSS = css`
/* Color the icon if unavailable */ /* Color the icon if unavailable */
ha-state-icon[data-state="unavailable"] { ha-state-icon[data-state="unavailable"] {
color: rgb(var(--rgb-state-unavailable-color)); color: var(--state-unavailable-color);
} }
`; `;

View File

@ -29,11 +29,9 @@ export type LocalizeKeys =
| `ui.panel.config.devices.${string}` | `ui.panel.config.devices.${string}`
| `ui.panel.config.energy.${string}` | `ui.panel.config.energy.${string}`
| `ui.panel.config.info.${string}` | `ui.panel.config.info.${string}`
| `ui.panel.config.logs.${string}`
| `ui.panel.config.lovelace.${string}` | `ui.panel.config.lovelace.${string}`
| `ui.panel.config.network.${string}` | `ui.panel.config.network.${string}`
| `ui.panel.config.scene.${string}` | `ui.panel.config.scene.${string}`
| `ui.panel.config.url.${string}`
| `ui.panel.config.zha.${string}` | `ui.panel.config.zha.${string}`
| `ui.panel.config.zwave_js.${string}` | `ui.panel.config.zwave_js.${string}`
| `ui.panel.lovelace.card.${string}` | `ui.panel.lovelace.card.${string}`
@ -67,19 +65,21 @@ export interface FormatsType {
const loadedPolyfillLocale = new Set(); const loadedPolyfillLocale = new Set();
const locale = getLocalLanguage();
const polyfills: Promise<any>[] = []; const polyfills: Promise<any>[] = [];
if (__BUILD__ === "latest") { if (__BUILD__ === "latest") {
if (shouldPolyfillLocale()) { if (shouldPolyfillLocale()) {
polyfills.push(import("@formatjs/intl-locale/polyfill")); await import("@formatjs/intl-locale/polyfill");
} }
if (shouldPolyfillPluralRules()) { if (shouldPolyfillPluralRules(locale)) {
polyfills.push(import("@formatjs/intl-pluralrules/polyfill")); polyfills.push(import("@formatjs/intl-pluralrules/polyfill"));
polyfills.push(import("@formatjs/intl-pluralrules/locale-data/en")); polyfills.push(import("@formatjs/intl-pluralrules/locale-data/en"));
} }
if (shouldPolyfillRelativeTime()) { if (shouldPolyfillRelativeTime(locale)) {
polyfills.push(import("@formatjs/intl-relativetimeformat/polyfill")); polyfills.push(import("@formatjs/intl-relativetimeformat/polyfill"));
} }
if (shouldPolyfillDateTime()) { if (shouldPolyfillDateTime(locale)) {
polyfills.push(import("@formatjs/intl-datetimeformat/polyfill")); polyfills.push(import("@formatjs/intl-datetimeformat/polyfill"));
polyfills.push(import("@formatjs/intl-datetimeformat/add-all-tz")); polyfills.push(import("@formatjs/intl-datetimeformat/add-all-tz"));
} }
@ -90,7 +90,7 @@ export const polyfillsLoaded =
? undefined ? undefined
: Promise.all(polyfills).then(() => : Promise.all(polyfills).then(() =>
// Load the default language // Load the default language
loadPolyfillLocales(getLocalLanguage()) loadPolyfillLocales(locale)
); );
/** /**
@ -216,7 +216,7 @@ export const loadPolyfillLocales = async (language: string) => {
// @ts-ignore // @ts-ignore
Intl.DateTimeFormat.__addLocaleData(await result.json()); Intl.DateTimeFormat.__addLocaleData(await result.json());
} }
} catch (_e) { } catch (e) {
// Ignore // Ignore
} }
}; };

View File

@ -1,3 +1,7 @@
import { differenceInDays, differenceInWeeks, startOfWeek } from "date-fns/esm";
import { FrontendLocaleData } from "../../data/translation";
import { firstWeekdayIndex } from "../datetime/first_weekday";
export type Unit = export type Unit =
| "second" | "second"
| "minute" | "minute"
@ -11,13 +15,13 @@ export type Unit =
const MS_PER_SECOND = 1e3; const MS_PER_SECOND = 1e3;
const SECS_PER_MIN = 60; const SECS_PER_MIN = 60;
const SECS_PER_HOUR = SECS_PER_MIN * 60; const SECS_PER_HOUR = SECS_PER_MIN * 60;
const SECS_PER_DAY = SECS_PER_HOUR * 24;
const SECS_PER_WEEK = SECS_PER_DAY * 7;
// Adapted from https://github.com/formatjs/formatjs/blob/186cef62f980ec66252ee232f438a42d0b51b9f9/packages/intl-utils/src/diff.ts // Adapted from https://github.com/formatjs/formatjs/blob/186cef62f980ec66252ee232f438a42d0b51b9f9/packages/intl-utils/src/diff.ts
export function selectUnit( export function selectUnit(
from: Date | number, from: Date | number,
// eslint-disable-next-line @typescript-eslint/default-param-last
to: Date | number = Date.now(), to: Date | number = Date.now(),
locale: FrontendLocaleData,
thresholds: Partial<Thresholds> = {} thresholds: Partial<Thresholds> = {}
): { value: number; unit: Unit } { ): { value: number; unit: Unit } {
const resolvedThresholds: Thresholds = { const resolvedThresholds: Thresholds = {
@ -49,29 +53,56 @@ export function selectUnit(
}; };
} }
const days = secs / SECS_PER_DAY; const fromDate = new Date(from);
const toDate = new Date(to);
// Set time component to zero, which allows us to compare only the days
fromDate.setHours(0, 0, 0, 0);
toDate.setHours(0, 0, 0, 0);
const days = differenceInDays(fromDate, toDate);
if (days === 0) {
return {
value: Math.round(hours),
unit: "hour",
};
}
if (Math.abs(days) < resolvedThresholds.day) { if (Math.abs(days) < resolvedThresholds.day) {
return { return {
value: Math.round(days), value: days,
unit: "day", unit: "day",
}; };
} }
const weeks = secs / SECS_PER_WEEK; const firstWeekday = firstWeekdayIndex(locale);
const fromWeek = startOfWeek(fromDate, { weekStartsOn: firstWeekday });
const toWeek = startOfWeek(toDate, { weekStartsOn: firstWeekday });
const weeks = differenceInWeeks(fromWeek, toWeek);
if (weeks === 0) {
return {
value: days,
unit: "day",
};
}
if (Math.abs(weeks) < resolvedThresholds.week) { if (Math.abs(weeks) < resolvedThresholds.week) {
return { return {
value: Math.round(weeks), value: weeks,
unit: "week", unit: "week",
}; };
} }
const fromDate = new Date(from);
const toDate = new Date(to);
const years = fromDate.getFullYear() - toDate.getFullYear(); const years = fromDate.getFullYear() - toDate.getFullYear();
const months = years * 12 + fromDate.getMonth() - toDate.getMonth(); const months = years * 12 + fromDate.getMonth() - toDate.getMonth();
if (Math.round(Math.abs(months)) < resolvedThresholds.month) { if (months === 0) {
return { return {
value: Math.round(months), value: weeks,
unit: "week",
};
}
if (Math.abs(months) < resolvedThresholds.month || years === 0) {
return {
value: months,
unit: "month", unit: "month",
}; };
} }

View File

@ -10,6 +10,8 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { clamp } from "../../common/number/clamp"; import { clamp } from "../../common/number/clamp";
import { computeRTL } from "../../common/util/compute_rtl";
import { HomeAssistant } from "../../types";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
@ -22,6 +24,8 @@ interface Tooltip extends TooltipModel<any> {
export default class HaChartBase extends LitElement { export default class HaChartBase extends LitElement {
public chart?: Chart; public chart?: Chart;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "chart-type", reflect: true }) @property({ attribute: "chart-type", reflect: true })
public chartType: ChartType = "line"; public chartType: ChartType = "line";
@ -33,6 +37,8 @@ export default class HaChartBase extends LitElement {
@property({ type: Number }) public height?: number; @property({ type: Number }) public height?: number;
@property({ type: Number }) public paddingYAxis = 0;
@state() private _chartHeight?: number; @state() private _chartHeight?: number;
@state() private _tooltip?: Tooltip; @state() private _tooltip?: Tooltip;
@ -128,6 +134,8 @@ export default class HaChartBase extends LitElement {
style=${styleMap({ style=${styleMap({
height: `${this.height ?? this._chartHeight}px`, height: `${this.height ?? this._chartHeight}px`,
overflow: this._chartHeight ? "initial" : "hidden", overflow: this._chartHeight ? "initial" : "hidden",
"padding-left": `${computeRTL(this.hass) ? 0 : this.paddingYAxis}px`,
"padding-right": `${computeRTL(this.hass) ? this.paddingYAxis : 0}px`,
})} })}
> >
<canvas></canvas> <canvas></canvas>
@ -225,7 +233,11 @@ export default class HaChartBase extends LitElement {
{ {
id: "afterRenderHook", id: "afterRenderHook",
afterRender: (chart) => { afterRender: (chart) => {
this._chartHeight = chart.height; const change = chart.height - (this._chartHeight ?? 0);
if (!this._chartHeight || change > 0 || change < -12) {
// hysteresis to prevent infinite render loops
this._chartHeight = chart.height;
}
}, },
legend: { legend: {
...this.options?.plugins?.legend, ...this.options?.plugins?.legend,

View File

@ -2,6 +2,8 @@ import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { html, LitElement, PropertyValues } from "lit"; import { html, LitElement, PropertyValues } from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import { getGraphColorByIndex } from "../../common/color/colors"; import { getGraphColorByIndex } from "../../common/color/colors";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTL } from "../../common/util/compute_rtl";
import { import {
formatNumber, formatNumber,
numberFormatToLocale, numberFormatToLocale,
@ -20,7 +22,7 @@ class StateHistoryChartLine extends LitElement {
@property({ attribute: false }) public data: LineChartEntity[] = []; @property({ attribute: false }) public data: LineChartEntity[] = [];
@property() public names: boolean | Record<string, string> = false; @property() public names?: Record<string, string>;
@property() public unit?: string; @property() public unit?: string;
@ -30,17 +32,25 @@ class StateHistoryChartLine extends LitElement {
@property({ attribute: false }) public endTime!: Date; @property({ attribute: false }) public endTime!: Date;
@property({ type: Number }) public paddingYAxis = 0;
@property({ type: Number }) public chartIndex?;
@state() private _chartData?: ChartData<"line">; @state() private _chartData?: ChartData<"line">;
@state() private _chartOptions?: ChartOptions; @state() private _chartOptions?: ChartOptions;
@state() private _yWidth = 0;
private _chartTime: Date = new Date(); private _chartTime: Date = new Date();
protected render() { protected render() {
return html` return html`
<ha-chart-base <ha-chart-base
.hass=${this.hass}
.data=${this._chartData} .data=${this._chartData}
.options=${this._chartOptions} .options=${this._chartOptions}
.paddingYAxis=${this.paddingYAxis - this._yWidth}
chart-type="line" chart-type="line"
></ha-chart-base> ></ha-chart-base>
`; `;
@ -84,6 +94,16 @@ class StateHistoryChartLine extends LitElement {
display: true, display: true,
text: this.unit, text: this.unit,
}, },
afterUpdate: (y) => {
if (this._yWidth !== Math.floor(y.width)) {
this._yWidth = Math.floor(y.width);
fireEvent(this, "y-width-changed", {
value: this._yWidth,
chartIndex: this.chartIndex,
});
}
},
position: computeRTL(this.hass) ? "right" : "left",
}, },
}, },
plugins: { plugins: {

View File

@ -2,6 +2,7 @@ import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import { fireEvent } from "../../common/dom/fire_event";
import { numberFormatToLocale } from "../../common/number/format_number"; import { numberFormatToLocale } from "../../common/number/format_number";
import { computeRTL } from "../../common/util/compute_rtl"; import { computeRTL } from "../../common/util/compute_rtl";
import { TimelineEntity } from "../../data/history"; import { TimelineEntity } from "../../data/history";
@ -18,7 +19,7 @@ export class StateHistoryChartTimeline extends LitElement {
@property() public narrow!: boolean; @property() public narrow!: boolean;
@property() public names: boolean | Record<string, string> = false; @property() public names?: Record<string, string>;
@property() public unit?: string; @property() public unit?: string;
@ -32,18 +33,26 @@ export class StateHistoryChartTimeline extends LitElement {
@property({ attribute: false }) public endTime!: Date; @property({ attribute: false }) public endTime!: Date;
@property({ type: Number }) public paddingYAxis = 0;
@property({ type: Number }) public chartIndex?;
@state() private _chartData?: ChartData<"timeline">; @state() private _chartData?: ChartData<"timeline">;
@state() private _chartOptions?: ChartOptions<"timeline">; @state() private _chartOptions?: ChartOptions<"timeline">;
@state() private _yWidth = 0;
private _chartTime: Date = new Date(); private _chartTime: Date = new Date();
protected render() { protected render() {
return html` return html`
<ha-chart-base <ha-chart-base
.hass=${this.hass}
.data=${this._chartData} .data=${this._chartData}
.options=${this._chartOptions} .options=${this._chartOptions}
.height=${this.data.length * 30 + 30} .height=${this.data.length * 30 + 30}
.paddingYAxis=${this.paddingYAxis - this._yWidth}
chart-type="timeline" chart-type="timeline"
></ha-chart-base> ></ha-chart-base>
`; `;
@ -55,6 +64,8 @@ export class StateHistoryChartTimeline extends LitElement {
} }
if ( if (
changedProps.has("startTime") ||
changedProps.has("endTime") ||
changedProps.has("data") || changedProps.has("data") ||
this._chartTime < this._chartTime <
new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES) new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES)
@ -131,6 +142,15 @@ export class StateHistoryChartTimeline extends LitElement {
scaleInstance.width = narrow ? 105 : 185; scaleInstance.width = narrow ? 105 : 185;
} }
}, },
afterUpdate: (y) => {
if (this._yWidth !== Math.floor(y.width)) {
this._yWidth = Math.floor(y.width);
fireEvent(this, "y-width-changed", {
value: this._yWidth,
chartIndex: this.chartIndex,
});
}
},
position: computeRTL(this.hass) ? "right" : "left", position: computeRTL(this.hass) ? "right" : "left",
}, },
}, },

View File

@ -31,15 +31,21 @@ const chunkData = (inputArray: any[], chunks: number) =>
return results; return results;
}, []); }, []);
declare global {
interface HASSDomEvents {
"y-width-changed": { value: number; chartIndex: number };
}
}
@customElement("state-history-charts") @customElement("state-history-charts")
class StateHistoryCharts extends LitElement { export class StateHistoryCharts extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public historyData!: HistoryResult; @property({ attribute: false }) public historyData!: HistoryResult;
@property() public narrow!: boolean; @property() public narrow!: boolean;
@property({ type: Boolean }) public names = false; @property() public names?: Record<string, string>;
@property({ type: Boolean, attribute: "virtualize", reflect: true }) @property({ type: Boolean, attribute: "virtualize", reflect: true })
public virtualize = false; public virtualize = false;
@ -56,10 +62,15 @@ class StateHistoryCharts extends LitElement {
@state() private _computedEndTime!: Date; @state() private _computedEndTime!: Date;
@state() private _maxYWidth = 0;
@state() private _childYWidths: number[] = [];
@state() private _chartCount = 0;
// @ts-ignore // @ts-ignore
@restoreScroll(".container") private _savedScrollPos?: number; @restoreScroll(".container") private _savedScrollPos?: number;
@eventOptions({ passive: true })
protected render(): TemplateResult { protected render(): TemplateResult {
if (!isComponentLoaded(this.hass, "history")) { if (!isComponentLoaded(this.hass, "history")) {
return html`<div class="info"> return html`<div class="info">
@ -99,6 +110,8 @@ class StateHistoryCharts extends LitElement {
).concat(this.historyData.line) ).concat(this.historyData.line)
: this.historyData.line; : this.historyData.line;
this._chartCount = combinedItems.length;
return this.virtualize return this.virtualize
? html`<div class="container ha-scrollbar" @scroll=${this._saveScrollPos}> ? html`<div class="container ha-scrollbar" @scroll=${this._saveScrollPos}>
<lit-virtualizer <lit-virtualizer
@ -130,7 +143,10 @@ class StateHistoryCharts extends LitElement {
.identifier=${item.identifier} .identifier=${item.identifier}
.showNames=${this.showNames} .showNames=${this.showNames}
.endTime=${this._computedEndTime} .endTime=${this._computedEndTime}
.paddingYAxis=${this._maxYWidth}
.names=${this.names} .names=${this.names}
.chartIndex=${index}
@y-width-changed=${this._yWidthChanged}
></state-history-chart-line> ></state-history-chart-line>
</div> `; </div> `;
} }
@ -144,6 +160,9 @@ class StateHistoryCharts extends LitElement {
.names=${this.names} .names=${this.names}
.narrow=${this.narrow} .narrow=${this.narrow}
.chunked=${this.virtualize} .chunked=${this.virtualize}
.paddingYAxis=${this._maxYWidth}
.chartIndex=${index}
@y-width-changed=${this._yWidthChanged}
></state-history-chart-timeline> ></state-history-chart-timeline>
</div> `; </div> `;
}; };
@ -152,6 +171,21 @@ class StateHistoryCharts extends LitElement {
return !(changedProps.size === 1 && changedProps.has("hass")); return !(changedProps.size === 1 && changedProps.has("hass"));
} }
protected updated(changedProps: PropertyValues) {
if (changedProps.has("_chartCount")) {
if (this._chartCount < this._childYWidths.length) {
this._childYWidths.length = this._chartCount;
this._maxYWidth =
this._childYWidths.length === 0 ? 0 : Math.max(...this._childYWidths);
}
}
}
private _yWidthChanged(e: CustomEvent<HASSDomEvents["y-width-changed"]>) {
this._childYWidths[e.detail.chartIndex] = e.detail.value;
this._maxYWidth = Math.max(...this._childYWidths);
}
private _isHistoryEmpty(): boolean { private _isHistoryEmpty(): boolean {
const historyDataEmpty = const historyDataEmpty =
!this.historyData || !this.historyData ||

View File

@ -66,7 +66,7 @@ class StatisticsChart extends LitElement {
StatisticsMetaData StatisticsMetaData
>; >;
@property() public names: boolean | Record<string, string> = false; @property() public names?: Record<string, string>;
@property() public unit?: string; @property() public unit?: string;
@ -133,6 +133,7 @@ class StatisticsChart extends LitElement {
return html` return html`
<ha-chart-base <ha-chart-base
.hass=${this.hass}
.data=${this._chartData} .data=${this._chartData}
.options=${this._chartOptions} .options=${this._chartOptions}
.chartType=${this.chartType} .chartType=${this.chartType}

View File

@ -1,11 +1,11 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { getGraphColorByIndex } from "../../../common/color/colors"; import { getGraphColorByIndex } from "../../../common/color/colors";
import { lab2hex, rgb2hex, rgb2lab } from "../../../common/color/convert-color"; import { hex2rgb, lab2hex, rgb2lab } from "../../../common/color/convert-color";
import { labBrighten } from "../../../common/color/lab"; import { labBrighten } from "../../../common/color/lab";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active"; import { stateColorProperties } from "../../../common/entity/state_color";
import { stateColor } from "../../../common/entity/state_color"; import { UNAVAILABLE, UNKNOWN } from "../../../data/entity";
import { UNAVAILABLE } from "../../../data/entity"; import { computeCssValue } from "../../../resources/css-variables";
const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = { const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
media_player: { media_player: {
@ -17,61 +17,35 @@ const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
}, },
}; };
const cssColorMap: Map<string, [number, number, number]> = new Map();
function cssToRgb(
cssVariable: string,
computedStyles: CSSStyleDeclaration
): [number, number, number] | undefined {
if (!cssVariable.startsWith("--rgb")) {
return undefined;
}
if (cssColorMap.has(cssVariable)) {
return cssColorMap.get(cssVariable)!;
}
const value = computedStyles.getPropertyValue(cssVariable);
if (!value) return undefined;
const rgb = value.split(",").map((v) => Number(v)) as [
number,
number,
number
];
cssColorMap.set(cssVariable, rgb);
return rgb;
}
function computeTimelineStateColor( function computeTimelineStateColor(
state: string, state: string,
computedStyles: CSSStyleDeclaration, computedStyles: CSSStyleDeclaration,
stateObj?: HassEntity stateObj?: HassEntity
): string | undefined { ): string | undefined {
if (!stateObj || state === UNAVAILABLE) { if (!stateObj || state === UNAVAILABLE) {
return "transparent"; return computeCssValue("--history-unavailable-color", computedStyles);
} }
const color = stateColor(stateObj, state); if (state === UNKNOWN) {
return computeCssValue("--history-unknown-color", computedStyles);
if (!color && !stateActive(stateObj, state)) {
const rgb = cssToRgb("--rgb-state-inactive-color", computedStyles);
if (!rgb) return undefined;
return rgb2hex(rgb);
} }
const rgb = cssToRgb(`--rgb-state-${color}-color`, computedStyles); const properties = stateColorProperties(stateObj, state);
if (!properties) {
return undefined;
}
const rgb = computeCssValue(properties, computedStyles);
if (!rgb) return undefined; if (!rgb) return undefined;
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
const shade = DOMAIN_STATE_SHADES[domain]?.[state] as number | number; const shade = DOMAIN_STATE_SHADES[domain]?.[state] as number | number;
if (!shade) { if (!shade) {
return rgb2hex(rgb); return rgb;
} }
return lab2hex(labBrighten(rgb2lab(rgb), shade)); return lab2hex(labBrighten(rgb2lab(hex2rgb(rgb)), shade));
} }
let colorIndex = 0; let colorIndex = 0;

View File

@ -200,7 +200,6 @@ export class HaDataTable extends LitElement {
Object.values(clonedColumns).forEach( Object.values(clonedColumns).forEach(
(column: ClonedDataTableColumnData) => { (column: ClonedDataTableColumnData) => {
delete column.title; delete column.title;
delete column.type;
delete column.template; delete column.template;
} }
); );

View File

@ -55,11 +55,16 @@ const sortData = (
? b[column.valueColumn || sortColumn][column.filterKey] ? b[column.valueColumn || sortColumn][column.filterKey]
: b[column.valueColumn || sortColumn]; : b[column.valueColumn || sortColumn];
if (typeof valA === "string") { if (column.type === "numeric") {
valA = valA.toUpperCase(); valA = isNaN(valA) ? undefined : Number(valA);
} valB = isNaN(valB) ? undefined : Number(valB);
if (typeof valB === "string") { } else {
valB = valB.toUpperCase(); if (typeof valA === "string") {
valA = valA.toUpperCase();
}
if (typeof valB === "string") {
valB = valB.toUpperCase();
}
} }
// Ensure "undefined" is always sorted to the bottom // Ensure "undefined" is always sorted to the bottom

View File

@ -1,6 +1,7 @@
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit"; import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { isValidEntityId } from "../../common/entity/valid_entity_id"; import { isValidEntityId } from "../../common/entity/valid_entity_id";
import type { PolymerChangedEvent } from "../../polymer-types"; import type { PolymerChangedEvent } from "../../polymer-types";
@ -95,7 +96,10 @@ class HaEntitiesPickerLight extends LitElement {
.excludeEntities=${this.excludeEntities} .excludeEntities=${this.excludeEntities}
.includeDeviceClasses=${this.includeDeviceClasses} .includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement} .includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter} .entityFilter=${this._getEntityFilter(
this.value,
this.entityFilter
)}
.value=${entityId} .value=${entityId}
.label=${this.pickedEntityLabel} .label=${this.pickedEntityLabel}
.disabled=${this.disabled} .disabled=${this.disabled}
@ -114,7 +118,7 @@ class HaEntitiesPickerLight extends LitElement {
.excludeEntities=${this.excludeEntities} .excludeEntities=${this.excludeEntities}
.includeDeviceClasses=${this.includeDeviceClasses} .includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement} .includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter} .entityFilter=${this._getEntityFilter(this.value, this.entityFilter)}
.label=${this.pickEntityLabel} .label=${this.pickEntityLabel}
.helper=${this.helper} .helper=${this.helper}
.disabled=${this.disabled} .disabled=${this.disabled}
@ -125,11 +129,15 @@ class HaEntitiesPickerLight extends LitElement {
`; `;
} }
private _entityFilter: HaEntityPickerEntityFilterFunc = ( private _getEntityFilter = memoizeOne(
stateObj: HassEntity (
) => value: string[] | undefined,
(!this.value || !this.value.includes(stateObj.entity_id)) && entityFilter: HaEntityPickerEntityFilterFunc | undefined
(!this.entityFilter || this.entityFilter(stateObj)); ): HaEntityPickerEntityFilterFunc =>
(stateObj: HassEntity) =>
(!value || !value.includes(stateObj.entity_id)) &&
(!entityFilter || entityFilter(stateObj))
);
private get _currentEntities() { private get _currentEntities() {
return this.value || []; return this.value || [];

View File

@ -1,4 +1,4 @@
import "@material/mwc-list/mwc-list-item"; import "../ha-list-item";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
@ -24,13 +24,13 @@ export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
// eslint-disable-next-line lit/prefer-static-styles // eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) => const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
html`<mwc-list-item graphic="avatar" .twoline=${!!item.entity_id}> html`<ha-list-item graphic="avatar" .twoline=${!!item.entity_id}>
${item.state ${item.state
? html`<state-badge slot="graphic" .stateObj=${item}></state-badge>` ? html`<state-badge slot="graphic" .stateObj=${item}></state-badge>`
: ""} : ""}
<span>${item.friendly_name}</span> <span>${item.friendly_name}</span>
<span slot="secondary">${item.entity_id}</span> <span slot="secondary">${item.entity_id}</span>
</mwc-list-item>`; </ha-list-item>`;
@customElement("ha-entity-picker") @customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement { export class HaEntityPicker extends LitElement {

View File

@ -22,6 +22,7 @@ import {
isNumericState, isNumericState,
} from "../../common/number/format_number"; } from "../../common/number/format_number";
import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { timerTimeRemaining } from "../../data/timer"; import { timerTimeRemaining } from "../../data/timer";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-label-badge"; import "../ha-label-badge";
@ -34,9 +35,9 @@ const TRUNCATED_DOMAINS = [
"person", "person",
] as const satisfies ReadonlyArray<keyof typeof FIXED_DOMAIN_STATES>; ] as const satisfies ReadonlyArray<keyof typeof FIXED_DOMAIN_STATES>;
type TruncatedDomain = typeof TRUNCATED_DOMAINS[number]; type TruncatedDomain = (typeof TRUNCATED_DOMAINS)[number];
type TruncatedKey = { type TruncatedKey = {
[T in TruncatedDomain]: `${T}.${typeof FIXED_DOMAIN_STATES[T][number]}`; [T in TruncatedDomain]: `${T}.${(typeof FIXED_DOMAIN_STATES)[T][number]}`;
}[TruncatedDomain]; }[TruncatedDomain];
const getTruncatedKey = (domainKey: string, stateKey: string) => { const getTruncatedKey = (domainKey: string, stateKey: string) => {
@ -103,8 +104,10 @@ export class HaStateLabelBadge extends LitElement {
// 4. Icon determined via entity state // 4. Icon determined via entity state
// 5. Value string as fallback // 5. Value string as fallback
const domain = computeStateDomain(entityState); const domain = computeStateDomain(entityState);
const entry = this.hass?.entities[entityState.entity_id];
const showIcon = this.icon || this._computeShowIcon(domain, entityState); const showIcon =
this.icon || this._computeShowIcon(domain, entityState, entry);
const image = this.icon const image = this.icon
? "" ? ""
: this.image : this.image
@ -112,7 +115,9 @@ export class HaStateLabelBadge extends LitElement {
: entityState.attributes.entity_picture_local || : entityState.attributes.entity_picture_local ||
entityState.attributes.entity_picture; entityState.attributes.entity_picture;
const value = const value =
!image && !showIcon ? this._computeValue(domain, entityState) : undefined; !image && !showIcon
? this._computeValue(domain, entityState, entry)
: undefined;
return html` return html`
<ha-label-badge <ha-label-badge
@ -152,7 +157,11 @@ export class HaStateLabelBadge extends LitElement {
} }
} }
private _computeValue(domain: string, entityState: HassEntity) { private _computeValue(
domain: string,
entityState: HassEntity,
entry?: EntityRegistryEntry
) {
switch (domain) { switch (domain) {
case "alarm_control_panel": case "alarm_control_panel":
case "binary_sensor": case "binary_sensor":
@ -165,7 +174,7 @@ export class HaStateLabelBadge extends LitElement {
return null; return null;
// @ts-expect-error we don't break and go to default // @ts-expect-error we don't break and go to default
case "sensor": case "sensor":
if (entityState.attributes.device_class === "moon__phase") { if (entry?.platform === "moon") {
return null; return null;
} }
// eslint-disable-next-line: disable=no-fallthrough // eslint-disable-next-line: disable=no-fallthrough
@ -177,7 +186,7 @@ export class HaStateLabelBadge extends LitElement {
? formatNumber( ? formatNumber(
entityState.state, entityState.state,
this.hass!.locale, this.hass!.locale,
getNumberFormatOptions(entityState) getNumberFormatOptions(entityState, entry)
) )
: computeStateDisplay( : computeStateDisplay(
this.hass!.localize, this.hass!.localize,
@ -188,7 +197,11 @@ export class HaStateLabelBadge extends LitElement {
} }
} }
private _computeShowIcon(domain: string, entityState: HassEntity): boolean { private _computeShowIcon(
domain: string,
entityState: HassEntity,
entry?: EntityRegistryEntry
): boolean {
if (entityState.state === UNAVAILABLE) { if (entityState.state === UNAVAILABLE) {
return false; return false;
} }
@ -204,7 +217,7 @@ export class HaStateLabelBadge extends LitElement {
case "timer": case "timer":
return true; return true;
case "sensor": case "sensor":
return entityState.attributes.device_class === "moon__phase"; return entry?.platform === "moon";
default: default:
return false; return false;
} }

View File

@ -11,13 +11,12 @@ import {
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { CLIMATE_HVAC_ACTION_COLORS } from "../../common/entity/color/climate_color";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { stateActive } from "../../common/entity/state_active";
import { stateColorCss } from "../../common/entity/state_color"; import { stateColorCss } from "../../common/entity/state_color";
import { iconColorCSS } from "../../common/style/icon_color_css"; import { iconColorCSS } from "../../common/style/icon_color_css";
import { cameraUrlWithWidthHeight } from "../../data/camera"; import { cameraUrlWithWidthHeight } from "../../data/camera";
import { HVAC_ACTION_TO_MODE } from "../../data/climate";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-state-icon"; import "../ha-state-icon";
@ -112,10 +111,10 @@ export class StateBadge extends LitElement {
} else if (this.color) { } else if (this.color) {
// Externally provided overriding color wins over state color // Externally provided overriding color wins over state color
iconStyle.color = this.color; iconStyle.color = this.color;
} else if (this._stateColor && stateActive(stateObj)) { } else if (this._stateColor) {
const color = stateColorCss(stateObj); const color = stateColorCss(stateObj);
if (color) { if (color) {
iconStyle.color = `rgb(${color})`; iconStyle.color = color;
} }
if (stateObj.attributes.rgb_color) { if (stateObj.attributes.rgb_color) {
iconStyle.color = `rgb(${stateObj.attributes.rgb_color.join(",")})`; iconStyle.color = `rgb(${stateObj.attributes.rgb_color.join(",")})`;
@ -134,8 +133,11 @@ export class StateBadge extends LitElement {
} }
if (stateObj.attributes.hvac_action) { if (stateObj.attributes.hvac_action) {
const hvacAction = stateObj.attributes.hvac_action; const hvacAction = stateObj.attributes.hvac_action;
if (["heating", "cooling", "drying"].includes(hvacAction)) { if (hvacAction in HVAC_ACTION_TO_MODE) {
iconStyle.color = `rgb(${CLIMATE_HVAC_ACTION_COLORS[hvacAction]})`; iconStyle.color = stateColorCss(
stateObj,
HVAC_ACTION_TO_MODE[hvacAction]
)!;
} else { } else {
delete iconStyle.color; delete iconStyle.color;
} }
@ -170,6 +172,7 @@ export class StateBadge extends LitElement {
line-height: 40px; line-height: 40px;
vertical-align: middle; vertical-align: middle;
box-sizing: border-box; box-sizing: border-box;
--state-inactive-color: initial;
} }
:host(:focus) { :host(:focus) {
outline: none; outline: none;

View File

@ -37,13 +37,10 @@ class HaAlert extends LitElement {
@property({ type: Boolean }) public dismissable = false; @property({ type: Boolean }) public dismissable = false;
@property({ type: Boolean }) public rtl = false;
public render() { public render() {
return html` return html`
<div <div
class="issue-type ${classMap({ class="issue-type ${classMap({
rtl: this.rtl,
[this.alertType]: true, [this.alertType]: true,
})}" })}"
role="alert" role="alert"
@ -84,9 +81,6 @@ class HaAlert extends LitElement {
padding: 8px; padding: 8px;
display: flex; display: flex;
} }
.issue-type.rtl {
flex-direction: row-reverse;
}
.issue-type::after { .issue-type::after {
position: absolute; position: absolute;
top: 0; top: 0;
@ -104,15 +98,12 @@ class HaAlert extends LitElement {
.icon.no-title { .icon.no-title {
align-self: center; align-self: center;
} }
.issue-type.rtl > .content {
flex-direction: row-reverse;
text-align: right;
}
.content { .content {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
width: 100%; width: 100%;
text-align: var(--float-start);
} }
.action { .action {
z-index: 1; z-index: 1;
@ -124,10 +115,9 @@ class HaAlert extends LitElement {
word-break: break-word; word-break: break-word;
margin-left: 8px; margin-left: 8px;
margin-right: 0; margin-right: 0;
} margin-inline-start: 8px;
.issue-type.rtl > .content > .main-content { margin-inline-end: 0;
margin-left: 0; direction: var(--direction);
margin-right: 8px;
} }
.title { .title {
margin-top: 2px; margin-top: 2px;

View File

@ -126,6 +126,7 @@ export class HaAreaPicker extends LitElement {
area_id: "no_areas", area_id: "no_areas",
name: this.hass.localize("ui.components.area-picker.no_areas"), name: this.hass.localize("ui.components.area-picker.no_areas"),
picture: null, picture: null,
aliases: [],
}, },
]; ];
} }
@ -256,6 +257,7 @@ export class HaAreaPicker extends LitElement {
area_id: "no_areas", area_id: "no_areas",
name: this.hass.localize("ui.components.area-picker.no_match"), name: this.hass.localize("ui.components.area-picker.no_match"),
picture: null, picture: null,
aliases: [],
}, },
]; ];
} }
@ -268,6 +270,7 @@ export class HaAreaPicker extends LitElement {
area_id: "add_new", area_id: "add_new",
name: this.hass.localize("ui.components.area-picker.add_new"), name: this.hass.localize("ui.components.area-picker.add_new"),
picture: null, picture: null,
aliases: [],
}, },
]; ];
} }

View File

@ -271,13 +271,18 @@ export class HaBarSlider extends LitElement {
return css` return css`
:host { :host {
display: block; display: block;
--slider-bar-color: rgb(var(--rgb-primary-color)); --slider-bar-color: var(--primary-color);
--slider-bar-background: rgb(var(--rgb-disabled-color)); --slider-bar-background: var(--disabled-color);
--slider-bar-background-opacity: 0.2; --slider-bar-background-opacity: 0.2;
--slider-bar-thickness: 40px; --slider-bar-thickness: 40px;
--slider-bar-border-radius: 10px; --slider-bar-border-radius: 10px;
height: var(--slider-bar-thickness); height: var(--slider-bar-thickness);
width: 100%; width: 100%;
border-radius: var(--slider-bar-border-radius);
outline: none;
}
:host(:focus-visible) {
box-shadow: 0 0 0 2px var(--slider-bar-color);
} }
:host([vertical]) { :host([vertical]) {
width: var(--slider-bar-thickness); width: var(--slider-bar-thickness);
@ -396,7 +401,7 @@ export class HaBarSlider extends LitElement {
.slider .slider-track-cursor:after { .slider .slider-track-cursor:after {
display: block; display: block;
content: ""; content: "";
background-color: rgb(var(--rgb-secondary-text-color)); background-color: var(--secondary-text-color);
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;

View File

@ -92,8 +92,8 @@ export class HaBarSwitch extends LitElement {
return css` return css`
:host { :host {
display: block; display: block;
--switch-bar-on-color: rgb(var(--rgb-primary-color)); --switch-bar-on-color: var(--primary-color);
--switch-bar-off-color: rgb(var(--rgb-disabled-color)); --switch-bar-off-color: var(--disabled-color);
--switch-bar-background-opacity: 0.2; --switch-bar-background-opacity: 0.2;
--switch-bar-thickness: 40px; --switch-bar-thickness: 40px;
--switch-bar-border-radius: 12px; --switch-bar-border-radius: 12px;
@ -104,6 +104,14 @@ export class HaBarSwitch extends LitElement {
box-sizing: border-box; box-sizing: border-box;
user-select: none; user-select: none;
cursor: pointer; cursor: pointer;
border-radius: var(--switch-bar-border-radius);
outline: none;
}
:host(:focus-visible) {
box-shadow: 0 0 0 2px var(--switch-bar-off-color);
}
:host([checked]:focus-visible) {
box-shadow: 0 0 0 2px var(--switch-bar-on-color);
} }
.switch { .switch {
box-sizing: border-box; box-sizing: border-box;

View File

@ -41,9 +41,9 @@ class HaBluePrintPicker extends LitElement {
return []; return [];
} }
const result = Object.entries(blueprints) const result = Object.entries(blueprints)
.filter(([_path, blueprint]) => !("error" in blueprint)) .filter((entry): entry is [string, Blueprint] => !("error" in entry[1]))
.map(([path, blueprint]) => ({ .map(([path, blueprint]) => ({
...(blueprint as Blueprint).metadata, ...blueprint.metadata,
path, path,
})); }));
return result.sort((a, b) => return result.sort((a, b) =>

View File

@ -0,0 +1,24 @@
import { Button } from "@material/mwc-button";
import { css } from "lit";
import { customElement } from "lit/decorators";
import { styles } from "@material/mwc-button/styles.css";
@customElement("ha-button")
export class HaButton extends Button {
static override styles = [
styles,
css`
::slotted([slot="icon"]) {
margin-inline-start: 0px;
margin-inline-end: 8px;
direction: var(--direction);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-button": HaButton;
}
}

View File

@ -13,6 +13,15 @@ export class HaCheckListItem extends CheckListItemBase {
:host { :host {
--mdc-theme-secondary: var(--primary-color); --mdc-theme-secondary: var(--primary-color);
} }
:host([graphic="avatar"]) .mdc-deprecated-list-item__graphic,
:host([graphic="medium"]) .mdc-deprecated-list-item__graphic,
:host([graphic="large"]) .mdc-deprecated-list-item__graphic,
:host([graphic="control"]) .mdc-deprecated-list-item__graphic {
margin-inline-end: var(--mdc-list-item-graphic-margin, 16px);
margin-inline-start: 0px;
direction: var(--direction);
}
`, `,
]; ];
} }

View File

@ -17,11 +17,8 @@ export class HaClickableListItem extends HaListItem {
const href = this.href || ""; const href = this.href || "";
return html`${this.disableHref return html`${this.disableHref
? html`<a aria-role="option">${r}</a>` ? html`<a>${r}</a>`
: html`<a : html`<a target=${this.openNewTab ? "_blank" : ""} href=${href}
aria-role="option"
target=${this.openNewTab ? "_blank" : ""}
href=${href}
>${r}</a >${r}</a
>`}`; >`}`;
} }

View File

@ -3,6 +3,7 @@ import { customElement, property } from "lit/decorators";
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display"; import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../common/entity/compute_state_display"; import { computeStateDisplay } from "../common/entity/compute_state_display";
import { formatNumber } from "../common/number/format_number"; import { formatNumber } from "../common/number/format_number";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import { ClimateEntity, CLIMATE_PRESET_NONE } from "../data/climate"; import { ClimateEntity, CLIMATE_PRESET_NONE } from "../data/climate";
import { isUnavailableState } from "../data/entity"; import { isUnavailableState } from "../data/entity";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@ -47,6 +48,19 @@ class HaClimateState extends LitElement {
if (!this.hass || !this.stateObj) { if (!this.hass || !this.stateObj) {
return undefined; return undefined;
} }
if (
this.stateObj.attributes.current_temperature != null &&
this.stateObj.attributes.current_humidity != null
) {
return `${formatNumber(
this.stateObj.attributes.current_temperature,
this.hass.locale
)} ${this.hass.config.unit_system.temperature}/
${formatNumber(
this.stateObj.attributes.current_humidity,
this.hass.locale
)}${blankBeforePercent(this.hass.locale)}%`;
}
if (this.stateObj.attributes.current_temperature != null) { if (this.stateObj.attributes.current_temperature != null) {
return `${formatNumber( return `${formatNumber(
@ -59,7 +73,7 @@ class HaClimateState extends LitElement {
return `${formatNumber( return `${formatNumber(
this.stateObj.attributes.current_humidity, this.stateObj.attributes.current_humidity,
this.hass.locale this.hass.locale
)} %`; )}${blankBeforePercent(this.hass.locale)}%`;
} }
return undefined; return undefined;

View File

@ -1,4 +1,3 @@
import "@material/mwc-list/mwc-list-item";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import { ComboBoxLitRenderer, comboBoxRenderer } from "@vaadin/combo-box/lit"; import { ComboBoxLitRenderer, comboBoxRenderer } from "@vaadin/combo-box/lit";
import "@vaadin/combo-box/theme/material/vaadin-combo-box-light"; import "@vaadin/combo-box/theme/material/vaadin-combo-box-light";
@ -15,15 +14,15 @@ import { customElement, property, query } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield"; import type { HaTextField } from "./ha-textfield";
registerStyles( registerStyles(
"vaadin-combo-box-item", "vaadin-combo-box-item",
css` css`
:host { :host {
padding: 0; padding: 0 !important;
} }
:host([focused]:not([disabled])) { :host([focused]:not([disabled])) {
background-color: rgba(var(--rgb-primary-text-color, 0, 0, 0), 0.12); background-color: rgba(var(--rgb-primary-text-color, 0, 0, 0), 0.12);
@ -211,9 +210,9 @@ export class HaComboBox extends LitElement {
private _defaultRowRenderer: ComboBoxLitRenderer< private _defaultRowRenderer: ComboBoxLitRenderer<
string | Record<string, any> string | Record<string, any>
> = (item) => > = (item) =>
html`<mwc-list-item> html`<ha-list-item>
${this.itemLabelPath ? item[this.itemLabelPath] : item} ${this.itemLabelPath ? item[this.itemLabelPath] : item}
</mwc-list-item>`; </ha-list-item>`;
private _clearValue(ev: Event) { private _clearValue(ev: Event) {
ev.stopPropagation(); ev.stopPropagation();

View File

@ -24,7 +24,7 @@ export class HaDialogDatePicker extends LitElement {
@state() private _value?: string; @state() private _value?: string;
public async showDialog(params: datePickerDialogParams): Promise<void> { public async showDialog(params: datePickerDialogParams): Promise<void> {
// app-datpicker has a bug, that it removes its handlers when disconnected, but doesnt add them back when reconnected. // app-datepicker has a bug, that it removes its handlers when disconnected, but doesn't add them back when reconnected.
// So we need to wait for the next render to make sure the element is removed and re-created so the handlers are added. // So we need to wait for the next render to make sure the element is removed and re-created so the handlers are added.
await nextRender(); await nextRender();
this._params = params; this._params = params;

View File

@ -46,7 +46,10 @@ export class HaDialog extends DialogBase {
styles, styles,
css` css`
.mdc-dialog { .mdc-dialog {
--mdc-dialog-scroll-divider-color: var(--divider-color); --mdc-dialog-scroll-divider-color: var(
--dialog-scroll-divider-color,
var(--divider-color)
);
z-index: var(--dialog-z-index, 7); z-index: var(--dialog-z-index, 7);
-webkit-backdrop-filter: var(--dialog-backdrop-filter, none); -webkit-backdrop-filter: var(--dialog-backdrop-filter, none);
backdrop-filter: var(--dialog-backdrop-filter, none); backdrop-filter: var(--dialog-backdrop-filter, none);

View File

@ -75,7 +75,6 @@ export class HaFileUpload extends LitElement {
${this.icon ${this.icon
? html`<span ? html`<span
class="mdc-text-field__icon mdc-text-field__icon--leading" class="mdc-text-field__icon mdc-text-field__icon--leading"
tabindex="-1"
> >
<ha-icon-button <ha-icon-button
@click=${this._openFilePicker} @click=${this._openFilePicker}
@ -95,7 +94,6 @@ export class HaFileUpload extends LitElement {
${this.value ${this.value
? html`<span ? html`<span
class="mdc-text-field__icon mdc-text-field__icon--trailing" class="mdc-text-field__icon mdc-text-field__icon--trailing"
tabindex="1"
> >
<ha-icon-button <ha-icon-button
slot="suffix" slot="suffix"

View File

@ -1,22 +1,33 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-alert"; import "../ha-alert";
import "../ha-selector/ha-selector"; import "../ha-selector/ha-selector";
import "./ha-form-boolean";
import "./ha-form-constant";
import "./ha-form-float";
import "./ha-form-grid";
import "./ha-form-expandable";
import "./ha-form-integer";
import "./ha-form-multi_select";
import "./ha-form-positive_time_period_dict";
import "./ha-form-select";
import "./ha-form-string";
import { HaFormDataContainer, HaFormElement, HaFormSchema } from "./types"; import { HaFormDataContainer, HaFormElement, HaFormSchema } from "./types";
const LOAD_ELEMENTS = {
boolean: () => import("./ha-form-boolean"),
constant: () => import("./ha-form-constant"),
float: () => import("./ha-form-float"),
grid: () => import("./ha-form-grid"),
expandable: () => import("./ha-form-expandable"),
integer: () => import("./ha-form-integer"),
multi_select: () => import("./ha-form-multi_select"),
positive_time_period_dict: () =>
import("./ha-form-positive_time_period_dict"),
select: () => import("./ha-form-select"),
string: () => import("./ha-form-string"),
};
const getValue = (obj, item) => const getValue = (obj, item) =>
obj ? (!item.name ? obj : obj[item.name]) : null; obj ? (!item.name ? obj : obj[item.name]) : null;
@ -43,6 +54,8 @@ export class HaForm extends LitElement implements HaFormElement {
@property() public computeHelper?: (schema: any) => string | undefined; @property() public computeHelper?: (schema: any) => string | undefined;
@property() public localizeValue?: (key: string) => string;
public focus() { public focus() {
const root = this.shadowRoot?.querySelector(".root"); const root = this.shadowRoot?.querySelector(".root");
if (!root) { if (!root) {
@ -56,6 +69,17 @@ export class HaForm extends LitElement implements HaFormElement {
} }
} }
protected willUpdate(changedProps: PropertyValues) {
if (changedProps.has("schema") && this.schema) {
this.schema.forEach((item) => {
if ("selector" in item) {
return;
}
LOAD_ELEMENTS[item.type]?.();
});
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="root" part="root"> <div class="root" part="root">
@ -86,7 +110,9 @@ export class HaForm extends LitElement implements HaFormElement {
.value=${getValue(this.data, item)} .value=${getValue(this.data, item)}
.label=${this._computeLabel(item, this.data)} .label=${this._computeLabel(item, this.data)}
.disabled=${item.disabled || this.disabled || false} .disabled=${item.disabled || this.disabled || false}
.placeholder=${item.required ? "" : item.default}
.helper=${this._computeHelper(item)} .helper=${this._computeHelper(item)}
.localizeValue=${this.localizeValue}
.required=${item.required || false} .required=${item.required || false}
.context=${this._generateContext(item)} .context=${this._generateContext(item)}
></ha-selector>` ></ha-selector>`

View File

@ -1,6 +1,8 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { domainIcon } from "../../common/entity/domain_icon";
import { IconSelector } from "../../data/selector"; import { IconSelector } from "../../data/selector";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-icon-picker"; import "../ha-icon-picker";
@ -21,7 +23,22 @@ export class HaIconSelector extends LitElement {
@property({ type: Boolean }) public required = true; @property({ type: Boolean }) public required = true;
@property() public context?: {
icon_entity?: string;
};
protected render() { protected render() {
const iconEntity = this.context?.icon_entity;
const stateObj = iconEntity ? this.hass.states[iconEntity] : undefined;
const placeholder =
this.selector.icon?.placeholder || stateObj?.attributes.icon;
const fallbackPath =
!placeholder && stateObj
? domainIcon(computeDomain(iconEntity!), stateObj)
: undefined;
return html` return html`
<ha-icon-picker <ha-icon-picker
.hass=${this.hass} .hass=${this.hass}
@ -30,8 +47,8 @@ export class HaIconSelector extends LitElement {
.required=${this.required} .required=${this.required}
.disabled=${this.disabled} .disabled=${this.disabled}
.helper=${this.helper} .helper=${this.helper}
.fallbackPath=${this.selector.icon?.fallbackPath} .fallbackPath=${this.selector.icon?.fallbackPath ?? fallbackPath}
.placeholder=${this.selector.icon?.placeholder} .placeholder=${this.selector.icon?.placeholder ?? placeholder}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-icon-picker> ></ha-icon-picker>
`; `;

View File

@ -8,7 +8,7 @@ import { getSignedPath } from "../../data/auth";
import { import {
MediaClassBrowserSettings, MediaClassBrowserSettings,
MediaPickedEvent, MediaPickedEvent,
SUPPORT_BROWSE_MEDIA, MediaPlayerEntityFeature,
} from "../../data/media-player"; } from "../../data/media-player";
import type { MediaSelector, MediaSelectorValue } from "../../data/selector"; import type { MediaSelector, MediaSelectorValue } from "../../data/selector";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
@ -80,7 +80,8 @@ export class HaMediaSelector extends LitElement {
const supportsBrowse = const supportsBrowse =
!this.value?.entity_id || !this.value?.entity_id ||
(stateObj && supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA)); (stateObj &&
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
return html`<ha-entity-picker return html`<ha-entity-picker
.hass=${this.hass} .hass=${this.hass}

View File

@ -39,7 +39,7 @@ export class HaNumberSelector extends LitElement {
<ha-slider <ha-slider
.min=${this.selector.number?.min} .min=${this.selector.number?.min}
.max=${this.selector.number?.max} .max=${this.selector.number?.max}
.value=${this._value} .value=${this.value ?? ""}
.step=${this.selector.number?.step ?? 1} .step=${this.selector.number?.step ?? 1}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
@ -81,17 +81,11 @@ export class HaNumberSelector extends LitElement {
`; `;
} }
private get _value() {
return this.value ?? (this.selector.number?.min || 0);
}
private _handleInputChange(ev) { private _handleInputChange(ev) {
ev.stopPropagation(); ev.stopPropagation();
const value = const value =
ev.target.value === "" || isNaN(ev.target.value) ev.target.value === "" || isNaN(ev.target.value)
? this.required ? undefined
? this.selector.number?.min || 0
: undefined
: Number(ev.target.value); : Number(ev.target.value);
if (this.value === value) { if (this.value === value) {
return; return;

View File

@ -1,9 +1,10 @@
import { html, LitElement } from "lit"; import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-yaml-editor"; import "../ha-yaml-editor";
import "../ha-input-helper-text"; import "../ha-input-helper-text";
import type { HaYamlEditor } from "../ha-yaml-editor";
@customElement("ha-selector-object") @customElement("ha-selector-object")
export class HaObjectSelector extends LitElement { export class HaObjectSelector extends LitElement {
@ -21,6 +22,10 @@ export class HaObjectSelector extends LitElement {
@property({ type: Boolean }) public required = true; @property({ type: Boolean }) public required = true;
@query("ha-yaml-editor", true) private _yamlEditor!: HaYamlEditor;
private _valueChangedFromChild = false;
protected render() { protected render() {
return html`<ha-yaml-editor return html`<ha-yaml-editor
.hass=${this.hass} .hass=${this.hass}
@ -36,7 +41,16 @@ export class HaObjectSelector extends LitElement {
: ""} `; : ""} `;
} }
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("value") && !this._valueChangedFromChild) {
this._yamlEditor.setValue(this.value);
}
this._valueChangedFromChild = false;
}
private _handleChange(ev) { private _handleChange(ev) {
this._valueChangedFromChild = true;
const value = ev.target.value; const value = ev.target.value;
if (!ev.target.isValid) { if (!ev.target.isValid) {
return; return;

View File

@ -28,6 +28,8 @@ export class HaSelectSelector extends LitElement {
@property() public helper?: string; @property() public helper?: string;
@property() public localizeValue?: (key: string) => string;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true; @property({ type: Boolean }) public required = true;
@ -39,9 +41,21 @@ export class HaSelectSelector extends LitElement {
protected render() { protected render() {
const options = const options =
this.selector.select?.options.map((option) => this.selector.select?.options.map((option) =>
typeof option === "object" ? option : { value: option, label: option } typeof option === "object"
? (option as SelectOption)
: ({ value: option, label: option } as SelectOption)
) || []; ) || [];
const translationKey = this.selector.select?.translation_key;
if (this.localizeValue && translationKey) {
options.forEach((option) => {
option.label =
this.localizeValue!(`${translationKey}.options.${option.value}`) ||
option.label;
});
}
if (!this.selector.select?.custom_value && this._mode === "list") { if (!this.selector.select?.custom_value && this._mode === "list") {
if (!this.selector.select?.multiple) { if (!this.selector.select?.multiple) {
return html` return html`
@ -164,10 +178,11 @@ export class HaSelectSelector extends LitElement {
<ha-select <ha-select
fixedMenuPosition fixedMenuPosition
naturalMenuWidth naturalMenuWidth
.label=${this.label} .label=${this.label ?? ""}
.value=${this.value} .value=${this.value ?? ""}
.helper=${this.helper} .helper=${this.helper ?? ""}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required}
@closed=${stopPropagation} @closed=${stopPropagation}
@selected=${this._valueChanged} @selected=${this._valueChanged}
> >

View File

@ -9,6 +9,7 @@ import {
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { import {
DeviceRegistryEntry, DeviceRegistryEntry,
getDeviceIntegrationLookup, getDeviceIntegrationLookup,
@ -78,7 +79,7 @@ export class HaTargetSelector extends LitElement {
? [this.selector.target?.entity.device_class] ? [this.selector.target?.entity.device_class]
: undefined} : undefined}
.includeDomains=${this.selector.target?.entity?.domain .includeDomains=${this.selector.target?.entity?.domain
? [this.selector.target?.entity.domain] ? ensureArray(this.selector.target.entity.domain as string | string[])
: undefined} : undefined}
.disabled=${this.disabled} .disabled=${this.disabled}
></ha-target-picker>`; ></ha-target-picker>`;

View File

@ -51,6 +51,8 @@ export class HaSelector extends LitElement {
@property() public helper?: string; @property() public helper?: string;
@property() public localizeValue?: (key: string) => string;
@property() public placeholder?: any; @property() public placeholder?: any;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@ -86,6 +88,7 @@ export class HaSelector extends LitElement {
required: this.required, required: this.required,
helper: this.helper, helper: this.helper,
context: this.context, context: this.context,
localizeValue: this.localizeValue,
id: "selector", id: "selector",
})} })}
`; `;

View File

@ -151,6 +151,14 @@ export class HaServiceControl extends LitElement {
updatedDefaultValue = true; updatedDefaultValue = true;
this._value!.data![field.key] = false; this._value!.data![field.key] = false;
} }
if (
field.selector &&
field.default !== undefined &&
this._value!.data![field.key] === undefined
) {
updatedDefaultValue = true;
this._value!.data![field.key] = field.default;
}
}); });
} }
if (updatedDefaultValue) { if (updatedDefaultValue) {

View File

@ -8,6 +8,12 @@ import { customElement, property } from "lit/decorators";
export class HaTextArea extends TextAreaBase { export class HaTextArea extends TextAreaBase {
@property({ type: Boolean, reflect: true }) autogrow = false; @property({ type: Boolean, reflect: true }) autogrow = false;
firstUpdated() {
super.firstUpdated();
this.setAttribute("dir", document.dir);
}
updated(changedProperties: PropertyValues) { updated(changedProperties: PropertyValues) {
super.updated(changedProperties); super.updated(changedProperties);
if (this.autogrow && changedProperties.has("value")) { if (this.autogrow && changedProperties.has("value")) {
@ -47,6 +53,10 @@ export class HaTextArea extends TextAreaBase {
margin-top: 16px; margin-top: 16px;
margin-bottom: 16px; margin-bottom: 16px;
} }
:host([dir="rtl"]) .mdc-floating-label {
right: 16px;
left: initial;
}
`, `,
]; ];
} }

View File

@ -58,20 +58,32 @@ export class HaTimeInput extends LitElement {
const eventValue = ev.detail.value; const eventValue = ev.detail.value;
const useAMPM = useAmPm(this.locale); const useAMPM = useAmPm(this.locale);
let hours = eventValue.hours || 0; let value;
if (eventValue && useAMPM) {
if (eventValue.amPm === "PM" && hours < 12) { if (
hours += 12; !isNaN(eventValue.hours) ||
} !isNaN(eventValue.minutes) ||
if (eventValue.amPm === "AM" && hours === 12) { !isNaN(eventValue.seconds)
hours = 0; ) {
let hours = eventValue.hours || 0;
if (eventValue && useAMPM) {
if (eventValue.amPm === "PM" && hours < 12) {
hours += 12;
}
if (eventValue.amPm === "AM" && hours === 12) {
hours = 0;
}
} }
value = `${hours.toString().padStart(2, "0")}:${
eventValue.minutes
? eventValue.minutes.toString().padStart(2, "0")
: "00"
}:${
eventValue.seconds
? eventValue.seconds.toString().padStart(2, "0")
: "00"
}`;
} }
const value = `${hours.toString().padStart(2, "0")}:${
eventValue.minutes ? eventValue.minutes.toString().padStart(2, "0") : "00"
}:${
eventValue.seconds ? eventValue.seconds.toString().padStart(2, "0") : "00"
}`;
if (value === this.value) { if (value === this.value) {
return; return;

View File

@ -21,8 +21,8 @@ export class HaTileBadge extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {
--tile-badge-background-color: rgb(var(--rgb-primary-color)); --tile-badge-background-color: var(--primary-color);
--tile-badge-icon-color: rgb(var(--rgb-white-color)); --tile-badge-icon-color: var(--white-color);
--mdc-icon-size: 12px; --mdc-icon-size: 12px;
} }
.badge { .badge {

Some files were not shown because too many files have changed in this diff Show More