20230125.0 (#15200)

This commit is contained in:
Bram Kragten 2023-01-25 17:19:42 +01:00 committed by GitHub
commit 51a45dd3cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
191 changed files with 5991 additions and 4481 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

@ -12,3 +12,7 @@ updates:
interval: "daily" interval: "daily"
time: "06:00" time: "06:00"
open-pull-requests-limit: 5 open-pull-requests-limit: 5
ignore:
# Ignore rollup and plugins until everything else is updated
- dependency-name: "*rollup*"
- dependency-name: "@rollup/*"

View File

@ -22,12 +22,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.2.0 uses: actions/checkout@v3.3.0
with: with:
ref: dev ref: dev
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1 uses: actions/setup-node@v3.6.0
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn
@ -60,12 +60,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.2.0 uses: actions/checkout@v3.3.0
with: with:
ref: master ref: master
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1 uses: actions/setup-node@v3.6.0
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn

View File

@ -20,9 +20,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.2.0 uses: actions/checkout@v3.3.0
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1 uses: actions/setup-node@v3.6.0
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn
@ -44,9 +44,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.2.0 uses: actions/checkout@v3.3.0
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1 uses: actions/setup-node@v3.6.0
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn
@ -63,9 +63,9 @@ jobs:
needs: [lint, test] needs: [lint, test]
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.2.0 uses: actions/checkout@v3.3.0
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1 uses: actions/setup-node@v3.6.0
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn
@ -82,9 +82,9 @@ jobs:
needs: [lint, test] needs: [lint, test]
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.2.0 uses: actions/checkout@v3.3.0
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1 uses: actions/setup-node@v3.6.0
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn

View File

@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3.2.0 uses: actions/checkout@v3.3.0
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.

View File

@ -23,12 +23,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.2.0 uses: actions/checkout@v3.3.0
with: with:
ref: dev ref: dev
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1 uses: actions/setup-node@v3.6.0
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn
@ -61,12 +61,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.2.0 uses: actions/checkout@v3.3.0
with: with:
ref: master ref: master
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1 uses: actions/setup-node@v3.6.0
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn

View File

@ -17,10 +17,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.2.0 uses: actions/checkout@v3.3.0
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1 uses: actions/setup-node@v3.6.0
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn

View File

@ -22,10 +22,10 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.2.0 uses: actions/checkout@v3.3.0
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1 uses: actions/setup-node@v3.6.0
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn

View File

@ -21,7 +21,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.2.0 uses: actions/checkout@v3.3.0
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4 uses: actions/setup-python@v4
@ -29,7 +29,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1 uses: actions/setup-node@v3.6.0
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn

View File

@ -24,7 +24,7 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.2.0 uses: actions/checkout@v3.3.0
- name: Verify version - name: Verify version
uses: home-assistant/actions/helpers/verify-version@master uses: home-assistant/actions/helpers/verify-version@master
@ -35,7 +35,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.5.1 uses: actions/setup-node@v3.6.0
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: yarn cache: yarn

View File

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.2.0 uses: actions/checkout@v3.3.0
- name: Upload Translations - name: Upload Translations
run: | run: |

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

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

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

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

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

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

@ -25,21 +25,16 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "^6.0.0", "@braintree/sanitize-url": "^6.0.0",
"@codemirror/autocomplete": "^0.19.12", "@codemirror/autocomplete": "^6.4.0",
"@codemirror/commands": "^0.19.8", "@codemirror/commands": "^6.1.3",
"@codemirror/gutter": "^0.19.9", "@codemirror/language": "^6.3.2",
"@codemirror/highlight": "^0.19.7", "@codemirror/legacy-modes": "^6.3.1",
"@codemirror/history": "^0.19.2", "@codemirror/search": "^6.2.3",
"@codemirror/legacy-modes": "^0.19.0", "@codemirror/state": "^6.2.0",
"@codemirror/rectangular-selection": "^0.19.1", "@codemirror/view": "^6.7.1",
"@codemirror/search": "^0.19.6",
"@codemirror/state": "^0.19.6",
"@codemirror/stream-parser": "^0.19.5",
"@codemirror/text": "^0.19.6",
"@codemirror/view": "^0.19.40",
"@formatjs/intl-datetimeformat": "^4.2.5", "@formatjs/intl-datetimeformat": "^4.2.5",
"@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": "^7.2.5",
"@formatjs/intl-pluralrules": "^4.1.5", "@formatjs/intl-pluralrules": "^4.1.5",
"@formatjs/intl-relativetimeformat": "^9.3.2", "@formatjs/intl-relativetimeformat": "^9.3.2",
@ -49,34 +44,35 @@
"@fullcalendar/interaction": "5.9.0", "@fullcalendar/interaction": "5.9.0",
"@fullcalendar/list": "5.9.0", "@fullcalendar/list": "5.9.0",
"@fullcalendar/timegrid": "5.9.0", "@fullcalendar/timegrid": "5.9.0",
"@lit-labs/motion": "^1.0.2", "@lezer/highlight": "^1.1.3",
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch", "@lit-labs/motion": "^1.0.3",
"@material/chips": "14.0.0-canary.261f2db59.0", "@lit-labs/virtualizer": "^1.0.1",
"@material/data-table": "14.0.0-canary.261f2db59.0", "@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-button": "0.25.3", "@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-checkbox": "0.25.3", "@material/mwc-button": "^0.27.0",
"@material/mwc-circular-progress": "0.25.3", "@material/mwc-checkbox": "^0.27.0",
"@material/mwc-dialog": "0.25.3", "@material/mwc-circular-progress": "^0.27.0",
"@material/mwc-drawer": "^0.25.3", "@material/mwc-dialog": "^0.27.0",
"@material/mwc-fab": "0.25.3", "@material/mwc-drawer": "^0.27.0",
"@material/mwc-formfield": "0.25.3", "@material/mwc-fab": "^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-formfield": "^0.27.0",
"@material/mwc-linear-progress": "0.25.3", "@material/mwc-icon-button": "^0.27.0",
"@material/mwc-list": "^0.25.3", "@material/mwc-linear-progress": "^0.27.0",
"@material/mwc-menu": "0.25.3", "@material/mwc-list": "^0.27.0",
"@material/mwc-radio": "0.25.3", "@material/mwc-menu": "^0.27.0",
"@material/mwc-ripple": "0.25.3", "@material/mwc-radio": "^0.27.0",
"@material/mwc-select": "0.25.3", "@material/mwc-ripple": "^0.27.0",
"@material/mwc-slider": "0.25.3", "@material/mwc-select": "^0.27.0",
"@material/mwc-switch": "0.25.3", "@material/mwc-slider": "^0.27.0",
"@material/mwc-tab": "0.25.3", "@material/mwc-switch": "^0.27.0",
"@material/mwc-tab-bar": "0.25.3", "@material/mwc-tab": "^0.27.0",
"@material/mwc-textarea": "^0.25.3", "@material/mwc-tab-bar": "^0.27.0",
"@material/mwc-textfield": "0.25.3", "@material/mwc-textarea": "^0.27.0",
"@material/mwc-top-app-bar-fixed": "^0.25.3", "@material/mwc-textfield": "^0.27.0",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0", "@material/mwc-top-app-bar-fixed": "^0.27.0",
"@mdi/js": "7.0.96", "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@mdi/svg": "7.0.96", "@mdi/js": "7.1.96",
"@mdi/svg": "7.1.96",
"@polymer/app-layout": "^3.1.0", "@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1", "@polymer/iron-icon": "^3.0.1",
@ -91,43 +87,43 @@
"@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.5",
"@vaadin/vaadin-themable-mixin": "^23.2.9", "@vaadin/vaadin-themable-mixin": "^23.3.5",
"@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.2.0", "@vue/web-component-wrapper": "^1.3.0",
"@webcomponents/scoped-custom-element-registry": "^0.0.5", "@webcomponents/scoped-custom-element-registry": "^0.0.5",
"@webcomponents/webcomponentsjs": "^2.2.10", "@webcomponents/webcomponentsjs": "^2.2.10",
"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.3.1",
"core-js": "^3.15.2", "core-js": "^3.15.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": "^1.3.7",
"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.0.0",
"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.1",
"home-assistant-js-websocket": "^8.0.1", "home-assistant-js-websocket": "^8.0.1",
"idb-keyval": "^5.1.3", "idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1", "intl-messageformat": "^9.9.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"leaflet-draw": "^1.0.4", "leaflet-draw": "^1.0.4",
"lit": "^2.1.2", "lit": "^2.6.1",
"marked": "^4.0.12", "marked": "^4.0.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.3.0",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"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.1",
@ -141,19 +137,19 @@
"vue": "^2.6.12", "vue": "^2.6.12",
"vue2-daterange-picker": "^0.5.1", "vue2-daterange-picker": "^0.5.1",
"weekstart": "^1.1.0", "weekstart": "^1.1.0",
"workbox-cacheable-response": "^6.4.2", "workbox-cacheable-response": "^6.5.4",
"workbox-core": "^6.4.2", "workbox-core": "^6.5.4",
"workbox-expiration": "^6.4.2", "workbox-expiration": "^6.5.4",
"workbox-precaching": "^6.4.2", "workbox-precaching": "^6.5.4",
"workbox-routing": "^6.4.2", "workbox-routing": "^6.5.4",
"workbox-strategies": "^6.4.2", "workbox-strategies": "^6.5.4",
"xss": "^1.0.9" "xss": "^1.0.14"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.2", "@babel/core": "^7.20.2",
"@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.7",
"@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.2",
"@babel/plugin-proposal-optional-chaining": "^7.18.9", "@babel/plugin-proposal-optional-chaining": "^7.18.9",
@ -164,7 +160,7 @@
"@babel/preset-typescript": "^7.18.6", "@babel/preset-typescript": "^7.18.6",
"@koa/cors": "^3.1.0", "@koa/cors": "^3.1.0",
"@octokit/auth-oauth-device": "^4.0.2", "@octokit/auth-oauth-device": "^4.0.2",
"@octokit/rest": "^19.0.4", "@octokit/rest": "^19.0.5",
"@open-wc/dev-server-hmr": "^0.0.2", "@open-wc/dev-server-hmr": "^0.0.2",
"@rollup/plugin-babel": "^5.2.1", "@rollup/plugin-babel": "^5.2.1",
"@rollup/plugin-commonjs": "^11.1.0", "@rollup/plugin-commonjs": "^11.1.0",
@ -173,7 +169,7 @@
"@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.3",
"@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",
@ -190,7 +186,7 @@
"@web/dev-server-rollup": "^0.2.11", "@web/dev-server-rollup": "^0.2.11",
"babel-loader": "^9.1.0", "babel-loader": "^9.1.0",
"chai": "^4.3.4", "chai": "^4.3.4",
"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": "^14.0.0",
@ -200,10 +196,10 @@
"eslint-plugin-import": "^2.24.2", "eslint-plugin-import": "^2.24.2",
"eslint-plugin-lit": "^1.6.1", "eslint-plugin-lit": "^1.6.1",
"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": "^7.0.1", "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.6",
@ -211,55 +207,51 @@
"gulp-rename": "^2.0.0", "gulp-rename": "^2.0.0",
"gulp-zopfli-green": "^3.0.1", "gulp-zopfli-green": "^3.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.3.1",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lint-staged": "^13.0.3", "lint-staged": "^13.1.0",
"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": "^1.0.1",
"mocha": "^8.4.0", "mocha": "^8.4.0",
"object-hash": "^2.0.3", "object-hash": "^3.0.0",
"open": "^7.0.4", "open": "^8.4.0",
"pinst": "^3.0.0", "pinst": "^3.0.0",
"prettier": "^2.8.1", "prettier": "^2.8.3",
"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.0",
"systemjs": "^6.3.2", "systemjs": "^6.3.2",
"tar": "^6.1.11", "tar": "^6.1.11",
"terser-webpack-plugin": "^5.2.4", "terser-webpack-plugin": "^5.2.4",
"ts-lit-plugin": "^1.2.1", "ts-lit-plugin": "^1.2.1",
"typescript": "^4.9.3", "typescript": "^4.9.4",
"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": "^4.8.0",
"webpack-dev-server": "^4.3.0", "webpack-dev-server": "^4.11.1",
"webpack-manifest-plugin": "^4.0.2", "webpack-manifest-plugin": "^4.0.2",
"webpackbar": "^5.0.0-3", "webpackbar": "^5.0.0-3",
"workbox-build": "^6.4.2" "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", "@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 = "20230110.0" version = "20230125.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

@ -201,6 +201,7 @@ export const DOMAINS_WITH_CARD = [
export const SENSOR_ENTITIES = [ export const SENSOR_ENTITIES = [
"sensor", "sensor",
"binary_sensor", "binary_sensor",
"calendar",
"camera", "camera",
"device_tracker", "device_tracker",
"weather", "weather",

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

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

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

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

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,12 @@ 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,
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 +52,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>

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,
@ -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";
@ -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>
`; `;
@ -131,6 +140,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,6 +31,12 @@ 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 { class StateHistoryCharts extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -56,6 +62,12 @@ 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;
@ -99,6 +111,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 +144,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 +161,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 +172,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

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

@ -5,7 +5,6 @@ import DateRangePicker from "vue2-daterange-picker";
// @ts-ignore // @ts-ignore
import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css"; import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { Constructor } from "../types";
const Component = Vue.extend({ const Component = Vue.extend({
props: { props: {
@ -47,35 +46,26 @@ const Component = Vue.extend({
}, },
}, },
render(createElement) { render(createElement) {
// @ts-ignore // @ts-expect-error
return createElement(DateRangePicker, { return createElement(DateRangePicker, {
props: { props: {
// @ts-ignore
"time-picker": this.timePicker, "time-picker": this.timePicker,
// @ts-ignore
"auto-apply": this.autoApply, "auto-apply": this.autoApply,
opens: "right", opens: "right",
"show-dropdowns": false, "show-dropdowns": false,
// @ts-ignore
"time-picker24-hour": this.twentyfourHours, "time-picker24-hour": this.twentyfourHours,
// @ts-ignore
disabled: this.disabled, disabled: this.disabled,
// @ts-ignore
ranges: this.ranges ? {} : false, ranges: this.ranges ? {} : false,
"locale-data": { "locale-data": {
// @ts-ignore
firstDay: this.firstDay, firstDay: this.firstDay,
}, },
}, },
model: { model: {
value: { value: {
// @ts-ignore
startDate: this.startDate, startDate: this.startDate,
// @ts-ignore
endDate: this.endDate, endDate: this.endDate,
}, },
callback: (value) => { callback: (value) => {
// @ts-ignore
fireEvent(this.$el as HTMLElement, "change", value); fireEvent(this.$el as HTMLElement, "change", value);
}, },
expression: "dateRange", expression: "dateRange",
@ -106,7 +96,11 @@ const Component = Vue.extend({
}, },
}); });
const WrappedElement: Constructor<HTMLElement> = wrap(Vue, Component); // Assertion corrects HTMLElement type from package
const WrappedElement = wrap(
Vue,
Component
) as unknown as CustomElementConstructor;
@customElement("date-range-picker") @customElement("date-range-picker")
class DateRangePickerElement extends WrappedElement { class DateRangePickerElement extends WrappedElement {

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

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

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 (["heating", "cooling", "drying", "fan"].includes(hvacAction)) {
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

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

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

@ -4,6 +4,7 @@ import type {
CompletionResult, CompletionResult,
CompletionSource, CompletionSource,
} from "@codemirror/autocomplete"; } from "@codemirror/autocomplete";
import type { Extension } from "@codemirror/state";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view"; import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import { HassEntities } from "home-assistant-js-websocket"; import { HassEntities } from "home-assistant-js-websocket";
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit"; import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
@ -72,9 +73,9 @@ export class HaCodeEditor extends ReactiveElement {
if (!this.codemirror || !this._loadedCodeMirror) { if (!this.codemirror || !this._loadedCodeMirror) {
return false; return false;
} }
const className = this._loadedCodeMirror.HighlightStyle.get( const className = this._loadedCodeMirror.highlightingFor(
this.codemirror.state, this.codemirror.state,
this._loadedCodeMirror.tags.comment [this._loadedCodeMirror.tags.comment]
); );
return !!this.shadowRoot!.querySelector(`span.${className}`); return !!this.shadowRoot!.querySelector(`span.${className}`);
} }
@ -136,7 +137,7 @@ export class HaCodeEditor extends ReactiveElement {
private async _load(): Promise<void> { private async _load(): Promise<void> {
this._loadedCodeMirror = await loadCodeMirror(); this._loadedCodeMirror = await loadCodeMirror();
const extensions = [ const extensions: Extension[] = [
this._loadedCodeMirror.lineNumbers(), this._loadedCodeMirror.lineNumbers(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true), this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.history(), this._loadedCodeMirror.history(),
@ -152,10 +153,8 @@ export class HaCodeEditor extends ReactiveElement {
saveKeyBinding, saveKeyBinding,
] as KeyBinding[]), ] as KeyBinding[]),
this._loadedCodeMirror.langCompartment.of(this._mode), this._loadedCodeMirror.langCompartment.of(this._mode),
this._loadedCodeMirror.theme, this._loadedCodeMirror.haTheme,
this._loadedCodeMirror.Prec.fallback( this._loadedCodeMirror.haSyntaxHighlighting,
this._loadedCodeMirror.highlightStyle
),
this._loadedCodeMirror.readonlyCompartment.of( this._loadedCodeMirror.readonlyCompartment.of(
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly) this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
), ),
@ -227,7 +226,7 @@ export class HaCodeEditor extends ReactiveElement {
return { return {
from: Number(entityWord.from), from: Number(entityWord.from),
options: states, options: states,
span: /^[a-z_]{3,}\.\w*$/, validFor: /^[a-z_]{3,}\.\w*$/,
}; };
} }
@ -268,7 +267,7 @@ export class HaCodeEditor extends ReactiveElement {
return { return {
from: Number(match.from), from: Number(match.from),
options: iconItems, options: iconItems,
span: /^mdi:\S*$/, validFor: /^mdi:\S*$/,
}; };
} }

View File

@ -43,6 +43,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) {
@ -86,7 +88,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

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

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

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

View File

@ -82,9 +82,8 @@ export class HaTileButton extends LitElement {
return css` return css`
:host { :host {
--tile-button-icon-color: var(--primary-text-color); --tile-button-icon-color: var(--primary-text-color);
--tile-button-background-color: rgb(var(--rgb-disabled-color)); --tile-button-background-color: var(--disabled-color);
--tile-button-background-opacity: 0.2; --tile-button-background-opacity: 0.2;
--mdc-ripple-color: var(--tile-button-background-color);
width: 40px; width: 40px;
height: 40px; height: 40px;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
@ -107,6 +106,7 @@ export class HaTileButton extends LitElement {
outline: none; outline: none;
overflow: hidden; overflow: hidden;
background: none; background: none;
--mdc-ripple-color: var(--tile-button-background-color);
} }
.button::before { .button::before {
content: ""; content: "";
@ -128,7 +128,7 @@ export class HaTileButton extends LitElement {
} }
.button:disabled { .button:disabled {
cursor: not-allowed; cursor: not-allowed;
--tile-button-background-color: rgb(var(--rgb-disabled-color)); --tile-button-background-color: var(--disabled-color);
--tile-button-icon-color: var(--disabled-text-color); --tile-button-icon-color: var(--disabled-text-color);
--tile-button-background-opacity: 0.2; --tile-button-background-opacity: 0.2;
} }

View File

@ -22,7 +22,7 @@ export class HaTileIcon extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {
--tile-icon-color: rgb(var(--rgb-disabled-color)); --tile-icon-color: var(--disabled-color);
--mdc-icon-size: 24px; --mdc-icon-size: 24px;
} }
.shape::before { .shape::before {

View File

@ -47,13 +47,10 @@ export class HaTileSlider extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
ha-bar-slider { ha-bar-slider {
--slider-bar-color: var( --slider-bar-color: var(--tile-slider-color, var(--primary-color));
--tile-slider-color,
rgb(var(--rgb-primary-color))
);
--slider-bar-background: var( --slider-bar-background: var(
--tile-slider-background, --tile-slider-background,
rgb(var(--rgb-disabled-color)) var(--disabled-color)
); );
--slider-bar-background-opacity: var( --slider-bar-background-opacity: var(
--tile-slider-background-opacity, --tile-slider-background-opacity,

View File

@ -10,6 +10,7 @@ export interface AreaRegistryEntry {
area_id: string; area_id: string;
name: string; name: string;
picture: string | null; picture: string | null;
aliases: string[];
} }
export interface AreaEntityLookup { export interface AreaEntityLookup {
@ -23,6 +24,7 @@ export interface AreaDeviceLookup {
export interface AreaRegistryEntryMutableParams { export interface AreaRegistryEntryMutableParams {
name: string; name: string;
picture?: string | null; picture?: string | null;
aliases?: string[];
} }
export const createAreaRegistryEntry = ( export const createAreaRegistryEntry = (

View File

@ -8,7 +8,7 @@ import { BlueprintInput } from "./blueprint";
import { DeviceCondition, DeviceTrigger } from "./device_automation"; import { DeviceCondition, DeviceTrigger } from "./device_automation";
import { Action, MODES } from "./script"; import { Action, MODES } from "./script";
export const AUTOMATION_DEFAULT_MODE: typeof MODES[number] = "single"; export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
export const AUTOMATION_DEFAULT_MAX = 10; export const AUTOMATION_DEFAULT_MAX = 10;
export interface AutomationEntity extends HassEntityBase { export interface AutomationEntity extends HassEntityBase {
@ -29,7 +29,7 @@ export interface ManualAutomationConfig {
trigger: Trigger | Trigger[]; trigger: Trigger | Trigger[];
condition?: Condition | Condition[]; condition?: Condition | Condition[];
action: Action | Action[]; action: Action | Action[];
mode?: typeof MODES[number]; mode?: (typeof MODES)[number];
max?: number; max?: number;
max_exceeded?: max_exceeded?:
| "silent" | "silent"

View File

@ -1,200 +0,0 @@
import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types";
import {
computeHistory,
HistoryStates,
HistoryResult,
LineChartUnit,
TimelineEntity,
entityIdHistoryNeedsAttributes,
fetchRecentWS,
} from "./history";
export interface CacheConfig {
cacheKey: string;
hoursToShow: number;
}
interface CachedResults {
prom: Promise<HistoryResult>;
startTime: Date;
endTime: Date;
language: string;
data: HistoryResult;
}
const stateHistoryCache: { [cacheKey: string]: CachedResults } = {};
// Cache type 2 functionality
function getEmptyCache(
language: string,
startTime: Date,
endTime: Date
): CachedResults {
return {
prom: Promise.resolve({ line: [], timeline: [] }),
language,
startTime,
endTime,
data: { line: [], timeline: [] },
};
}
export const getRecentWithCache = (
hass: HomeAssistant,
entityIds: string[],
cacheConfig: CacheConfig,
localize: LocalizeFunc,
language: string
) => {
const cacheKey = cacheConfig.cacheKey;
const fullCacheKey = cacheKey + `_${cacheConfig.hoursToShow}`;
const endTime = new Date();
const startTime = new Date(endTime);
startTime.setHours(startTime.getHours() - cacheConfig.hoursToShow);
let toFetchStartTime = startTime;
let appendingToCache = false;
let cache = stateHistoryCache[fullCacheKey];
if (
cache &&
toFetchStartTime >= cache.startTime &&
toFetchStartTime <= cache.endTime &&
cache.language === language
) {
toFetchStartTime = cache.endTime;
appendingToCache = true;
// This pretty much never happens as endTime is usually set to now
if (endTime <= cache.endTime) {
return cache.prom;
}
} else {
cache = stateHistoryCache[fullCacheKey] = getEmptyCache(
language,
startTime,
endTime
);
}
const curCacheProm = cache.prom;
const noAttributes = !entityIds.some((entityId) =>
entityIdHistoryNeedsAttributes(hass, entityId)
);
const genProm = async () => {
let fetchedHistory: HistoryStates;
try {
const results = await Promise.all([
curCacheProm,
fetchRecentWS(
hass,
entityIds,
toFetchStartTime,
endTime,
appendingToCache,
undefined,
true,
noAttributes
),
]);
fetchedHistory = results[1];
} catch (err: any) {
delete stateHistoryCache[fullCacheKey];
throw err;
}
const stateHistory = computeHistory(hass, fetchedHistory, localize);
if (appendingToCache) {
if (stateHistory.line.length) {
mergeLine(stateHistory.line, cache.data.line);
}
if (stateHistory.timeline.length) {
mergeTimeline(stateHistory.timeline, cache.data.timeline);
// Replace the timeline array to force an update
cache.data.timeline = [...cache.data.timeline];
}
pruneStartTime(startTime, cache.data);
} else {
cache.data = stateHistory;
}
return cache.data;
};
cache.prom = genProm();
cache.startTime = startTime;
cache.endTime = endTime;
return cache.prom;
};
const mergeLine = (
historyLines: LineChartUnit[],
cacheLines: LineChartUnit[]
) => {
historyLines.forEach((line) => {
const unit = line.unit;
const oldLine = cacheLines.find((cacheLine) => cacheLine.unit === unit);
if (oldLine) {
line.data.forEach((entity) => {
const oldEntity = oldLine.data.find(
(cacheEntity) => entity.entity_id === cacheEntity.entity_id
);
if (oldEntity) {
oldEntity.states = oldEntity.states.concat(entity.states);
} else {
oldLine.data.push(entity);
}
});
// Replace the cached line data to force an update
oldLine.data = [...oldLine.data];
} else {
cacheLines.push(line);
}
});
};
const mergeTimeline = (
historyTimelines: TimelineEntity[],
cacheTimelines: TimelineEntity[]
) => {
historyTimelines.forEach((timeline) => {
const oldTimeline = cacheTimelines.find(
(cacheTimeline) => cacheTimeline.entity_id === timeline.entity_id
);
if (oldTimeline) {
oldTimeline.data = oldTimeline.data.concat(timeline.data);
} else {
cacheTimelines.push(timeline);
}
});
};
const pruneArray = (originalStartTime: Date, arr) => {
if (arr.length === 0) {
return arr;
}
const changedAfterStartTime = arr.findIndex(
(state) => new Date(state.last_changed) > originalStartTime
);
if (changedAfterStartTime === 0) {
// If all changes happened after originalStartTime then we are done.
return arr;
}
// If all changes happened at or before originalStartTime. Use last index.
const updateIndex =
changedAfterStartTime === -1 ? arr.length - 1 : changedAfterStartTime - 1;
arr[updateIndex].last_changed = originalStartTime;
return arr.slice(updateIndex);
};
const pruneStartTime = (originalStartTime: Date, cacheData: HistoryResult) => {
cacheData.line.forEach((line) => {
line.data.forEach((entity) => {
entity.states = pruneArray(originalStartTime, entity.states);
});
});
cacheData.timeline.forEach((timeline) => {
timeline.data = pruneArray(originalStartTime, timeline.data);
});
};

View File

@ -72,3 +72,12 @@ const hvacModeOrdering: { [key in HvacMode]: number } = {
export const compareClimateHvacModes = (mode1: HvacMode, mode2: HvacMode) => export const compareClimateHvacModes = (mode1: HvacMode, mode2: HvacMode) =>
hvacModeOrdering[mode1] - hvacModeOrdering[mode2]; hvacModeOrdering[mode1] - hvacModeOrdering[mode2];
export const HVAC_ACTION_TO_MODE: Record<HvacAction, HvacMode> = {
cooling: "cool",
drying: "dry",
fan: "fan_only",
heating: "heat",
idle: "off",
off: "off",
};

View File

@ -54,7 +54,6 @@ interface ConversationResult {
export interface AgentInfo { export interface AgentInfo {
attribution?: { name: string; url: string }; attribution?: { name: string; url: string };
onboarding?: { text: string; url: string };
} }
export const processConversationInput = ( export const processConversationInput = (
@ -76,11 +75,11 @@ export const getAgentInfo = (hass: HomeAssistant): Promise<AgentInfo> =>
type: "conversation/agent/info", type: "conversation/agent/info",
}); });
export const setConversationOnboarding = ( export const prepareConversation = (
hass: HomeAssistant, hass: HomeAssistant,
value: boolean language?: string
): Promise<boolean> => ): Promise<void> =>
hass.callWS({ hass.callWS({
type: "conversation/onboarding/set", type: "conversation/prepare",
shown: value, language,
}); });

View File

@ -186,8 +186,8 @@ export interface EnergyInfo {
export interface EnergyValidationIssue { export interface EnergyValidationIssue {
type: string; type: string;
identifier: string; affected_entities: [string, unknown][];
value?: unknown; translation_placeholders: Record<string, string>;
} }
export interface EnergyPreferencesValidation { export interface EnergyPreferencesValidation {
@ -200,10 +200,12 @@ export const getEnergyInfo = (hass: HomeAssistant) =>
type: "energy/info", type: "energy/info",
}); });
export const getEnergyPreferenceValidation = (hass: HomeAssistant) => export const getEnergyPreferenceValidation = async (hass: HomeAssistant) => {
hass.callWS<EnergyPreferencesValidation>({ await hass.loadBackendTranslation("issues", "energy");
return hass.callWS<EnergyPreferencesValidation>({
type: "energy/validate", type: "energy/validate",
}); });
};
export const getEnergyPreferences = (hass: HomeAssistant) => export const getEnergyPreferences = (hass: HomeAssistant) =>
hass.callWS<EnergyPreferences>({ hass.callWS<EnergyPreferences>({
@ -669,7 +671,7 @@ export const getEnergySolarForecasts = (hass: HomeAssistant) =>
}); });
const energyGasUnitClass = ["volume", "energy"] as const; const energyGasUnitClass = ["volume", "energy"] as const;
export type EnergyGasUnitClass = typeof energyGasUnitClass[number]; export type EnergyGasUnitClass = (typeof energyGasUnitClass)[number];
export const getEnergyGasUnitClass = ( export const getEnergyGasUnitClass = (
prefs: EnergyPreferences, prefs: EnergyPreferences,

View File

@ -30,6 +30,7 @@ export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
device_class?: string; device_class?: string;
original_device_class?: string; original_device_class?: string;
aliases: string[]; aliases: string[];
options: EntityRegistryOptions | null;
} }
export interface UpdateEntityRegistryEntryResult { export interface UpdateEntityRegistryEntryResult {
@ -39,6 +40,7 @@ export interface UpdateEntityRegistryEntryResult {
} }
export interface SensorEntityOptions { export interface SensorEntityOptions {
precision?: number | null;
unit_of_measurement?: string | null; unit_of_measurement?: string | null;
} }
@ -54,6 +56,12 @@ export interface WeatherEntityOptions {
wind_speed_unit?: string | null; wind_speed_unit?: string | null;
} }
export interface EntityRegistryOptions {
number?: NumberEntityOptions;
sensor?: SensorEntityOptions;
weather?: WeatherEntityOptions;
}
export interface EntityRegistryEntryUpdateParams { export interface EntityRegistryEntryUpdateParams {
name?: string | null; name?: string | null;
icon?: string | null; icon?: string | null;
@ -111,6 +119,15 @@ export const getExtendedEntityRegistryEntry = (
entity_id: entityId, entity_id: entityId,
}); });
export const getExtendedEntityRegistryEntries = (
hass: HomeAssistant,
entityIds: string[]
): Promise<Record<string, ExtEntityRegistryEntry>> =>
hass.callWS({
type: "config/entity_registry/get_entries",
entity_ids: entityIds,
});
export const updateEntityRegistryEntry = ( export const updateEntityRegistryEntry = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string, entityId: string,

View File

@ -17,6 +17,8 @@ const NEED_ATTRIBUTE_DOMAINS = [
"input_datetime", "input_datetime",
"thermostat", "thermostat",
"water_heater", "water_heater",
"person",
"device_tracker",
]; ];
const LINE_ATTRIBUTES_TO_KEEP = [ const LINE_ATTRIBUTES_TO_KEEP = [
"temperature", "temperature",
@ -68,7 +70,7 @@ export interface HistoryStates {
[entityId: string]: EntityHistoryState[]; [entityId: string]: EntityHistoryState[];
} }
interface EntityHistoryState { export interface EntityHistoryState {
/** state */ /** state */
s: string; s: string;
/** attributes */ /** attributes */
@ -79,6 +81,12 @@ interface EntityHistoryState {
lu: number; lu: number;
} }
export interface HistoryStreamMessage {
states: HistoryStates;
start_time?: number; // Start time of this historical chunk
end_time?: number; // End time of this historical chunk
}
export const entityIdHistoryNeedsAttributes = ( export const entityIdHistoryNeedsAttributes = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string entityId: string
@ -86,73 +94,6 @@ export const entityIdHistoryNeedsAttributes = (
!hass.states[entityId] || !hass.states[entityId] ||
NEED_ATTRIBUTE_DOMAINS.includes(computeDomain(entityId)); NEED_ATTRIBUTE_DOMAINS.includes(computeDomain(entityId));
export const fetchRecent = (
hass: HomeAssistant,
entityId: string,
startTime: Date,
endTime: Date,
skipInitialState = false,
significantChangesOnly?: boolean,
minimalResponse = true,
noAttributes?: boolean
): Promise<HassEntity[][]> => {
let url = "history/period";
if (startTime) {
url += "/" + startTime.toISOString();
}
url += "?filter_entity_id=" + entityId;
if (endTime) {
url += "&end_time=" + endTime.toISOString();
}
if (skipInitialState) {
url += "&skip_initial_state";
}
if (significantChangesOnly !== undefined) {
url += `&significant_changes_only=${Number(significantChangesOnly)}`;
}
if (minimalResponse) {
url += "&minimal_response";
}
if (noAttributes) {
url += "&no_attributes";
}
return hass.callApi("GET", url);
};
export const fetchRecentWS = (
hass: HomeAssistant,
entityIds: string[],
startTime: Date,
endTime: Date,
skipInitialState = false,
significantChangesOnly?: boolean,
minimalResponse = true,
noAttributes?: boolean
) =>
hass.callWS<HistoryStates>({
type: "history/history_during_period",
start_time: startTime.toISOString(),
end_time: endTime.toISOString(),
significant_changes_only: significantChangesOnly || false,
include_start_time_state: !skipInitialState,
minimal_response: minimalResponse,
no_attributes: noAttributes || false,
entity_ids: entityIds,
});
export const fetchDate = (
hass: HomeAssistant,
startTime: Date,
endTime: Date,
entityIds: string[]
): Promise<HassEntity[][]> =>
hass.callApi(
"GET",
`history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response${
entityIds ? `&filter_entity_id=${entityIds.join(",")}` : ``
}`
);
export const fetchDateWS = ( export const fetchDateWS = (
hass: HomeAssistant, hass: HomeAssistant,
startTime: Date, startTime: Date,
@ -174,6 +115,142 @@ export const fetchDateWS = (
return hass.callWS<HistoryStates>(params); return hass.callWS<HistoryStates>(params);
}; };
export const subscribeHistory = (
hass: HomeAssistant,
callbackFunction: (message: HistoryStreamMessage) => void,
startTime: Date,
endTime: Date,
entityIds: string[]
): Promise<() => Promise<void>> => {
const params = {
type: "history/stream",
entity_ids: entityIds,
start_time: startTime.toISOString(),
end_time: endTime.toISOString(),
minimal_response: true,
no_attributes: !entityIds.some((entityId) =>
entityIdHistoryNeedsAttributes(hass, entityId)
),
};
return hass.connection.subscribeMessage<HistoryStreamMessage>(
(message) => callbackFunction(message),
params
);
};
class HistoryStream {
hass: HomeAssistant;
hoursToShow: number;
combinedHistory: HistoryStates;
constructor(hass: HomeAssistant, hoursToShow: number) {
this.hass = hass;
this.hoursToShow = hoursToShow;
this.combinedHistory = {};
}
processMessage(streamMessage: HistoryStreamMessage): HistoryStates {
if (!this.combinedHistory || !Object.keys(this.combinedHistory).length) {
this.combinedHistory = streamMessage.states;
return this.combinedHistory;
}
if (!Object.keys(streamMessage.states).length) {
// Empty messages are still sent to
// indicate no more historical events
return this.combinedHistory;
}
const purgeBeforePythonTime =
(new Date().getTime() - 60 * 60 * this.hoursToShow * 1000) / 1000;
const newHistory: HistoryStates = {};
for (const entityId of Object.keys(this.combinedHistory)) {
newHistory[entityId] = [];
}
for (const entityId of Object.keys(streamMessage.states)) {
newHistory[entityId] = [];
}
for (const entityId of Object.keys(newHistory)) {
if (
entityId in this.combinedHistory &&
entityId in streamMessage.states
) {
const entityCombinedHistory = this.combinedHistory[entityId];
const lastEntityCombinedHistory =
entityCombinedHistory[entityCombinedHistory.length - 1];
newHistory[entityId] = entityCombinedHistory.concat(
streamMessage.states[entityId]
);
if (
streamMessage.states[entityId][0].lu < lastEntityCombinedHistory.lu
) {
// If the history is out of order we have to sort it.
newHistory[entityId] = newHistory[entityId].sort(
(a, b) => a.lu - b.lu
);
}
} else if (entityId in this.combinedHistory) {
newHistory[entityId] = this.combinedHistory[entityId];
} else {
newHistory[entityId] = streamMessage.states[entityId];
}
// Remove old history
if (entityId in this.combinedHistory) {
const expiredStates = newHistory[entityId].filter(
(state) => state.lu < purgeBeforePythonTime
);
if (!expiredStates.length) {
continue;
}
newHistory[entityId] = newHistory[entityId].filter(
(state) => state.lu >= purgeBeforePythonTime
);
if (
newHistory[entityId].length &&
newHistory[entityId][0].lu === purgeBeforePythonTime
) {
continue;
}
// Update the first entry to the start time state
// as we need to preserve the start time state and
// only expire the rest of the history as it ages.
const lastExpiredState = expiredStates[expiredStates.length - 1];
lastExpiredState.lu = purgeBeforePythonTime;
newHistory[entityId].unshift(lastExpiredState);
}
}
this.combinedHistory = newHistory;
return this.combinedHistory;
}
}
export const subscribeHistoryStatesTimeWindow = (
hass: HomeAssistant,
callbackFunction: (data: HistoryStates) => void,
hoursToShow: number,
entityIds: string[],
minimalResponse = true,
significantChangesOnly = true
): Promise<() => Promise<void>> => {
const params = {
type: "history/stream",
entity_ids: entityIds,
start_time: new Date(
new Date().getTime() - 60 * 60 * hoursToShow * 1000
).toISOString(),
minimal_response: minimalResponse,
significant_changes_only: significantChangesOnly,
no_attributes: !entityIds.some((entityId) =>
entityIdHistoryNeedsAttributes(hass, entityId)
),
};
const stream = new HistoryStream(hass, hoursToShow);
return hass.connection.subscribeMessage<HistoryStreamMessage>(
(message) => callbackFunction(stream.processMessage(message)),
params
);
};
const equalState = (obj1: LineChartState, obj2: LineChartState) => const equalState = (obj1: LineChartState, obj2: LineChartState) =>
obj1.state === obj2.state && obj1.state === obj2.state &&
// Only compare attributes if both states have an attributes object. // Only compare attributes if both states have an attributes object.

View File

@ -7,8 +7,8 @@ import { TranslationDict } from "../types";
import { UNAVAILABLE_STATES } from "./entity"; import { UNAVAILABLE_STATES } from "./entity";
type HumidifierState = type HumidifierState =
| typeof FIXED_DOMAIN_STATES.humidifier[number] | (typeof FIXED_DOMAIN_STATES.humidifier)[number]
| typeof UNAVAILABLE_STATES[number]; | (typeof UNAVAILABLE_STATES)[number];
type HumidifierMode = type HumidifierMode =
keyof TranslationDict["state_attributes"]["humidifier"]["mode"]; keyof TranslationDict["state_attributes"]["humidifier"]["mode"];

12
src/data/number.ts Normal file
View File

@ -0,0 +1,12 @@
import { HomeAssistant } from "../types";
export type NumberDeviceClassUnits = { units: string[] };
export const getNumberDeviceClassConvertibleUnits = (
hass: HomeAssistant,
deviceClass: string
): Promise<NumberDeviceClassUnits> =>
hass.callWS({
type: "number/device_class_convertible_units",
device_class: deviceClass,
});

11
src/data/otbr.ts Normal file
View File

@ -0,0 +1,11 @@
import { HomeAssistant } from "../types";
export interface OTBRInfo {
url: string;
active_dataset_tlvs: string;
}
export const getOTBRInfo = (hass: HomeAssistant): Promise<OTBRInfo> =>
hass.callWS({
type: "otbr/info",
});

View File

@ -98,7 +98,7 @@ const statisticTypes = [
"state", "state",
"sum", "sum",
] as const; ] as const;
export type StatisticsTypes = typeof statisticTypes[number][]; export type StatisticsTypes = (typeof statisticTypes)[number][];
export interface StatisticsValidationResults { export interface StatisticsValidationResults {
[statisticId: string]: StatisticsValidationResult[]; [statisticId: string]: StatisticsValidationResult[];

View File

@ -15,7 +15,7 @@ export interface ScheduleDay {
to: string; to: string;
} }
type ScheduleDays = { [K in typeof weekdays[number]]?: ScheduleDay[] }; type ScheduleDays = { [K in (typeof weekdays)[number]]?: ScheduleDay[] };
export interface Schedule extends ScheduleDays { export interface Schedule extends ScheduleDays {
id: string; id: string;

View File

@ -77,7 +77,7 @@ const activateSceneActionStruct: Describe<ServiceSceneAction> = assign(
export interface ScriptEntity extends HassEntityBase { export interface ScriptEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & { attributes: HassEntityAttributeBase & {
last_triggered: string; last_triggered: string;
mode: typeof MODES[number]; mode: (typeof MODES)[number];
current?: number; current?: number;
max?: number; max?: number;
}; };
@ -89,7 +89,7 @@ export interface ManualScriptConfig {
alias: string; alias: string;
sequence: Action | Action[]; sequence: Action | Action[];
icon?: string; icon?: string;
mode?: typeof MODES[number]; mode?: (typeof MODES)[number];
max?: number; max?: number;
} }

View File

@ -215,6 +215,7 @@ export interface SelectSelector {
custom_value?: boolean; custom_value?: boolean;
mode?: "list" | "dropdown"; mode?: "list" | "dropdown";
options: readonly string[] | readonly SelectOption[]; options: readonly string[] | readonly SelectOption[];
translation_key?: string;
} | null; } | null;
} }

View File

@ -1,2 +1,15 @@
import { HomeAssistant } from "../types";
export const SENSOR_DEVICE_CLASS_BATTERY = "battery"; export const SENSOR_DEVICE_CLASS_BATTERY = "battery";
export const SENSOR_DEVICE_CLASS_TIMESTAMP = "timestamp"; export const SENSOR_DEVICE_CLASS_TIMESTAMP = "timestamp";
export type SensorDeviceClassUnits = { units: string[] };
export const getSensorDeviceClassConvertibleUnits = (
hass: HomeAssistant,
deviceClass: string
): Promise<SensorDeviceClassUnits> =>
hass.callWS({
type: "sensor/device_class_convertible_units",
device_class: deviceClass,
});

View File

@ -1,9 +1,9 @@
import { HomeAssistant } from "../types"; import { HomeAssistant, TranslationDict } from "../types";
export interface LoggedError { export interface LoggedError {
name: string; name: string;
message: [string]; message: [string];
level: string; level: keyof TranslationDict["ui"]["panel"]["config"]["logs"]["level"];
source: [string, number]; source: [string, number];
// unix timestamp in seconds // unix timestamp in seconds
timestamp: number; timestamp: number;
@ -13,8 +13,13 @@ export interface LoggedError {
first_occurred: number; first_occurred: number;
} }
export const fetchSystemLog = (hass: HomeAssistant) => export const fetchSystemLog = async (hass: HomeAssistant) => {
hass.callWS<LoggedError[]>({ type: "system_log/list" }); const log = await hass.callWS<LoggedError[]>({ type: "system_log/list" });
for (const error of log) {
error.level = error.level.toLowerCase() as LoggedError["level"];
}
return log;
};
export const getLoggedErrorIntegration = (item: LoggedError) => { export const getLoggedErrorIntegration = (item: LoggedError) => {
// Try to derive from logger name // Try to derive from logger name

View File

@ -54,7 +54,8 @@ export type TranslationCategory =
| "system_health" | "system_health"
| "device_class" | "device_class"
| "application_credentials" | "application_credentials"
| "issues"; | "issues"
| "selector";
export const fetchTranslationPreferences = (hass: HomeAssistant) => export const fetchTranslationPreferences = (hass: HomeAssistant) =>
fetchFrontendUserData(hass.connection, "language"); fetchFrontendUserData(hass.connection, "language");

View File

@ -2,35 +2,34 @@ import "@material/mwc-button/mwc-button";
import { mdiDeleteOutline, mdiPlus } from "@mdi/js"; import { mdiDeleteOutline, mdiPlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeStateName } from "../../../../common/entity/compute_state_name"; import "../../components/ha-alert";
import "../../../../components/ha-alert"; import "../../components/ha-area-picker";
import "../../../../components/ha-area-picker"; import "../../components/ha-dialog";
import "../../../../components/ha-dialog"; import "../../components/ha-textfield";
import "../../../../components/ha-textfield"; import type { HaTextField } from "../../components/ha-textfield";
import type { HaTextField } from "../../../../components/ha-textfield"; import { haStyle, haStyleDialog } from "../../resources/styles";
import { haStyle, haStyleDialog } from "../../../../resources/styles"; import { HomeAssistant } from "../../types";
import { HomeAssistant } from "../../../../types"; import { AliasesDialogParams } from "./show-dialog-aliases";
import { EntityAliasesDialogParams } from "./show-dialog-entity-aliases";
@customElement("dialog-entity-aliases") @customElement("dialog-aliases")
class DialogEntityAliases extends LitElement { class DialogAliases extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: string; @state() private _error?: string;
@state() private _params?: EntityAliasesDialogParams; @state() private _params?: AliasesDialogParams;
@state() private _aliases!: string[]; @state() private _aliases!: string[];
@state() private _submitting = false; @state() private _submitting = false;
public async showDialog(params: EntityAliasesDialogParams): Promise<void> { public async showDialog(params: AliasesDialogParams): Promise<void> {
this._params = params; this._params = params;
this._error = undefined; this._error = undefined;
this._aliases = this._aliases =
this._params.entity.aliases?.length > 0 this._params.aliases?.length > 0
? this._params.entity.aliases ? [...this._params.aliases].sort()
: [""]; : [""];
await this.updateComplete; await this.updateComplete;
} }
@ -46,23 +45,17 @@ class DialogEntityAliases extends LitElement {
return html``; return html``;
} }
const entityId = this._params.entity.entity_id;
const stateObj = entityId ? this.hass.states[entityId] : undefined;
const name = (stateObj && computeStateName(stateObj)) || entityId;
return html` return html`
<ha-dialog <ha-dialog
open open
@closed=${this.closeDialog} @closed=${this.closeDialog}
.heading=${this.hass.localize( .heading=${this.hass.localize("ui.dialogs.aliases.heading", {
"ui.dialogs.entity_registry.editor.aliases.heading", name: this._params.name,
{ name } })}
)}
> >
<div> <div>
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert> ` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}
<div class="form"> <div class="form">
${this._aliases.map( ${this._aliases.map(
@ -73,7 +66,7 @@ class DialogEntityAliases extends LitElement {
.index=${index} .index=${index}
class="flex-auto" class="flex-auto"
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.dialogs.entity_registry.editor.aliases.input_label", "ui.dialogs.aliases.input_label",
{ number: index + 1 } { number: index + 1 }
)} )}
.value=${alias} .value=${alias}
@ -85,7 +78,7 @@ class DialogEntityAliases extends LitElement {
.index=${index} .index=${index}
slot="navigationIcon" slot="navigationIcon"
label=${this.hass!.localize( label=${this.hass!.localize(
"ui.dialogs.entity_registry.editor.aliases.remove_alias", "ui.dialogs.aliases.remove_alias",
{ number: index + 1 } { number: index + 1 }
)} )}
@click=${this._removeAlias} @click=${this._removeAlias}
@ -96,9 +89,7 @@ class DialogEntityAliases extends LitElement {
)} )}
<div class="layout horizontal center-center"> <div class="layout horizontal center-center">
<mwc-button @click=${this._addAlias}> <mwc-button @click=${this._addAlias}>
${this.hass!.localize( ${this.hass!.localize("ui.dialogs.aliases.add_alias")}
"ui.dialogs.entity_registry.editor.aliases.add_alias"
)}
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-button> </mwc-button>
</div> </div>
@ -113,12 +104,10 @@ class DialogEntityAliases extends LitElement {
</mwc-button> </mwc-button>
<mwc-button <mwc-button
slot="primaryAction" slot="primaryAction"
@click=${this._updateEntry} @click=${this._updateAliases}
.disabled=${this._submitting} .disabled=${this._submitting}
> >
${this.hass.localize( ${this.hass.localize("ui.dialogs.aliases.save")}
"ui.dialogs.entity_registry.editor.aliases.save"
)}
</mwc-button> </mwc-button>
</ha-dialog> </ha-dialog>
`; `;
@ -152,23 +141,18 @@ class DialogEntityAliases extends LitElement {
this._aliases = aliases; this._aliases = aliases;
} }
private async _updateEntry(): Promise<void> { private async _updateAliases(): Promise<void> {
this._submitting = true; this._submitting = true;
const noEmptyAliases = this._aliases const noEmptyAliases = this._aliases
.map((alias) => alias.trim()) .map((alias) => alias.trim())
.filter((alias) => alias); .filter((alias) => alias);
try { try {
await this._params!.updateEntry({ await this._params!.updateAliases(noEmptyAliases);
aliases: noEmptyAliases,
});
this.closeDialog(); this.closeDialog();
} catch (err: any) { } catch (err: any) {
this._error = this._error =
err.message || err.message || this.hass.localize("ui.dialogs.aliases.unknown_error");
this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases.unknown_error"
);
} finally { } finally {
this._submitting = false; this._submitting = false;
} }
@ -207,6 +191,6 @@ class DialogEntityAliases extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"dialog-entity-aliases": DialogEntityAliases; "dialog-aliases": DialogAliases;
} }
} }

View File

@ -0,0 +1,20 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface AliasesDialogParams {
name: string;
aliases: string[];
updateAliases: (aliases: string[]) => Promise<unknown>;
}
export const loadAliasesDialog = () => import("./dialog-aliases");
export const showAliasesDialog = (
element: HTMLElement,
aliasesParams: AliasesDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-aliases",
dialogImport: loadAliasesDialog,
dialogParams: aliasesParams,
});
};

View File

@ -24,6 +24,7 @@ export const showConfigFlowDialog = (
const [step] = await Promise.all([ const [step] = await Promise.all([
createConfigFlow(hass, handler), createConfigFlow(hass, handler),
hass.loadBackendTranslation("config", handler), hass.loadBackendTranslation("config", handler),
hass.loadBackendTranslation("selector", handler),
// Used as fallback if no header defined for step // Used as fallback if no header defined for step
hass.loadBackendTranslation("title", handler), hass.loadBackendTranslation("title", handler),
]); ]);
@ -32,6 +33,7 @@ export const showConfigFlowDialog = (
fetchFlow: async (hass, flowId) => { fetchFlow: async (hass, flowId) => {
const step = await fetchConfigFlow(hass, flowId); const step = await fetchConfigFlow(hass, flowId);
await hass.loadBackendTranslation("config", step.handler); await hass.loadBackendTranslation("config", step.handler);
await hass.loadBackendTranslation("selector", step.handler);
return step; return step;
}, },
handleFlowStep: handleConfigFlowStep, handleFlowStep: handleConfigFlowStep,
@ -95,6 +97,10 @@ export const showConfigFlowDialog = (
); );
}, },
renderShowFormStepFieldLocalizeValue(hass, step, key) {
return hass.localize(`component.${step.handler}.selector.${key}`);
},
renderExternalStepHeader(hass, step) { renderExternalStepHeader(hass, step) {
return ( return (
hass.localize( hass.localize(

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