mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 11:46:42 +00:00
20230628.0 (#17080)
This commit is contained in:
commit
50f4a1abc5
@ -10,6 +10,12 @@ supports es6-module-dynamic-import
|
|||||||
not Safari < 13
|
not Safari < 13
|
||||||
not iOS < 13
|
not iOS < 13
|
||||||
|
|
||||||
|
# Exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data
|
||||||
|
# Babel ignores these automatically, but we need here for Webpack to output ESM with dynamic imports
|
||||||
|
not KaiOS > 0
|
||||||
|
not QQAndroid > 0
|
||||||
|
not UCAndroid > 0
|
||||||
|
|
||||||
# Exclude unsupported browsers
|
# Exclude unsupported browsers
|
||||||
not dead
|
not dead
|
||||||
|
|
||||||
|
@ -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.10
|
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.11
|
||||||
|
|
||||||
ENV \
|
ENV \
|
||||||
DEBIAN_FRONTEND=noninteractive \
|
DEBIAN_FRONTEND=noninteractive \
|
||||||
|
5
.github/release-drafter.yml
vendored
5
.github/release-drafter.yml
vendored
@ -1,3 +1,8 @@
|
|||||||
|
categories:
|
||||||
|
- title: 'Dependency updates'
|
||||||
|
collapse-after: 3
|
||||||
|
labels:
|
||||||
|
- 'dependencies'
|
||||||
template: |
|
template: |
|
||||||
## What's Changed
|
## What's Changed
|
||||||
|
|
||||||
|
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
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.5.2
|
uses: actions/checkout@v3.5.3
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ 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.5.2
|
uses: actions/checkout@v3.5.3
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
|
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@ -24,7 +24,7 @@ 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.5.2
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.6.0
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
@ -47,7 +47,7 @@ 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.5.2
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.6.0
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
@ -65,7 +65,7 @@ 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.5.2
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.6.0
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
@ -83,7 +83,7 @@ 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.5.2
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.6.0
|
uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3.5.2
|
uses: actions/checkout@v3.5.3
|
||||||
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.
|
||||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@ -22,7 +22,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
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.5.2
|
uses: actions/checkout@v3.5.3
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ 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.5.2
|
uses: actions/checkout@v3.5.3
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
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.5.2
|
uses: actions/checkout@v3.5.3
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.6.0
|
uses: actions/setup-node@v3.6.0
|
||||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
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.5.2
|
uses: actions/checkout@v3.5.3
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3.6.0
|
uses: actions/setup-node@v3.6.0
|
||||||
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@ -9,7 +9,7 @@ jobs:
|
|||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v4.0.0
|
- uses: dessant/lock-threads@v4.0.1
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-lock-inactive-days: "30"
|
issue-lock-inactive-days: "30"
|
||||||
|
4
.github/workflows/nightly.yaml
vendored
4
.github/workflows/nightly.yaml
vendored
@ -6,7 +6,7 @@ on:
|
|||||||
- cron: "0 1 * * *"
|
- cron: "0 1 * * *"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PYTHON_VERSION: "3.10"
|
PYTHON_VERSION: "3.11"
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
NODE_OPTIONS: --max_old_space_size=6144
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@ -20,7 +20,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3.5.2
|
uses: actions/checkout@v3.5.3
|
||||||
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
|
9
.github/workflows/release-drafter.yaml
vendored
9
.github/workflows/release-drafter.yaml
vendored
@ -5,8 +5,17 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update_release_draft:
|
update_release_draft:
|
||||||
|
permissions:
|
||||||
|
# write permission for contents is required to create a github release
|
||||||
|
contents: write
|
||||||
|
# write permission for pull-requests is required for autolabeler
|
||||||
|
# otherwise, read permission is required at least
|
||||||
|
pull-requests: read
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: release-drafter/release-drafter@v5
|
- uses: release-drafter/release-drafter@v5
|
||||||
|
6
.github/workflows/release.yaml
vendored
6
.github/workflows/release.yaml
vendored
@ -6,7 +6,7 @@ on:
|
|||||||
- published
|
- published
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PYTHON_VERSION: "3.10"
|
PYTHON_VERSION: "3.11"
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
NODE_OPTIONS: --max_old_space_size=6144
|
||||||
|
|
||||||
# Set default workflow permissions
|
# Set default workflow permissions
|
||||||
@ -23,7 +23,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.5.2
|
uses: actions/checkout@v3.5.3
|
||||||
|
|
||||||
- name: Verify version
|
- name: Verify version
|
||||||
uses: home-assistant/actions/helpers/verify-version@master
|
uses: home-assistant/actions/helpers/verify-version@master
|
||||||
@ -76,7 +76,7 @@ jobs:
|
|||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2023.04.0
|
uses: home-assistant/wheels@2023.04.0
|
||||||
with:
|
with:
|
||||||
abi: cp310
|
abi: cp311
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
arch: amd64
|
arch: amd64
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3.5.2
|
uses: actions/checkout@v3.5.3
|
||||||
|
|
||||||
- name: Upload Translations
|
- name: Upload Translations
|
||||||
run: |
|
run: |
|
||||||
|
@ -77,6 +77,7 @@ module.exports.htmlMinifierOptions = {
|
|||||||
module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
|
module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
|
||||||
safari10: !latestBuild,
|
safari10: !latestBuild,
|
||||||
ecma: latestBuild ? 2015 : 5,
|
ecma: latestBuild ? 2015 : 5,
|
||||||
|
module: latestBuild,
|
||||||
format: { comments: false },
|
format: { comments: false },
|
||||||
sourceMap: !isTestBuild,
|
sourceMap: !isTestBuild,
|
||||||
});
|
});
|
||||||
@ -97,7 +98,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
|||||||
"@babel/preset-env",
|
"@babel/preset-env",
|
||||||
{
|
{
|
||||||
useBuiltIns: latestBuild ? false : "entry",
|
useBuiltIns: latestBuild ? false : "entry",
|
||||||
corejs: latestBuild ? false : { version: "3.30", proposals: true },
|
corejs: latestBuild ? false : { version: "3.31", proposals: true },
|
||||||
bugfixes: true,
|
bugfixes: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -17,6 +17,7 @@ const modules = {
|
|||||||
"intl-datetimeformat": "DateTimeFormat",
|
"intl-datetimeformat": "DateTimeFormat",
|
||||||
"intl-numberformat": "NumberFormat",
|
"intl-numberformat": "NumberFormat",
|
||||||
"intl-displaynames": "DisplayNames",
|
"intl-displaynames": "DisplayNames",
|
||||||
|
"intl-listformat": "ListFormat",
|
||||||
};
|
};
|
||||||
|
|
||||||
gulp.task("create-locale-data", (done) => {
|
gulp.task("create-locale-data", (done) => {
|
||||||
|
@ -142,4 +142,5 @@ module.exports = {
|
|||||||
createCastConfig,
|
createCastConfig,
|
||||||
createHassioConfig,
|
createHassioConfig,
|
||||||
createGalleryConfig,
|
createGalleryConfig,
|
||||||
|
createRollupConfig,
|
||||||
};
|
};
|
||||||
|
@ -41,7 +41,7 @@ const createWebpackConfig = ({
|
|||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
mode: isProdBuild ? "production" : "development",
|
mode: isProdBuild ? "production" : "development",
|
||||||
target: ["web", latestBuild ? "es2017" : "es5"],
|
target: `browserslist:${latestBuild ? "modern" : "legacy"}`,
|
||||||
// For tests/CI, source maps are skipped to gain build speed
|
// For tests/CI, source maps are skipped to gain build speed
|
||||||
// For production, generate source maps for accurate stack traces without source code
|
// For production, generate source maps for accurate stack traces without source code
|
||||||
// For development, generate "cheap" versions that can map to original line numbers
|
// For development, generate "cheap" versions that can map to original line numbers
|
||||||
@ -84,6 +84,13 @@ const createWebpackConfig = ({
|
|||||||
],
|
],
|
||||||
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
||||||
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
||||||
|
splitChunks: {
|
||||||
|
// Disable splitting for web workers with ESM output
|
||||||
|
// Imports of external chunks are broken
|
||||||
|
chunks: latestBuild
|
||||||
|
? (chunk) => !chunk.canBeInitial() && !/^.+-worker$/.test(chunk.name)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
!isStatsBuild && new WebpackBar({ fancy: !isProdBuild }),
|
!isStatsBuild && new WebpackBar({ fancy: !isProdBuild }),
|
||||||
@ -160,9 +167,12 @@ const createWebpackConfig = ({
|
|||||||
"lit/polyfill-support$": "lit/polyfill-support.js",
|
"lit/polyfill-support$": "lit/polyfill-support.js",
|
||||||
"@lit-labs/virtualizer/layouts/grid":
|
"@lit-labs/virtualizer/layouts/grid":
|
||||||
"@lit-labs/virtualizer/layouts/grid.js",
|
"@lit-labs/virtualizer/layouts/grid.js",
|
||||||
|
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver":
|
||||||
|
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
module: latestBuild,
|
||||||
filename: ({ chunk }) =>
|
filename: ({ chunk }) =>
|
||||||
!isProdBuild || isStatsBuild || dontHash.has(chunk.name)
|
!isProdBuild || isStatsBuild || dontHash.has(chunk.name)
|
||||||
? "[name].js"
|
? "[name].js"
|
||||||
@ -196,7 +206,7 @@ const createWebpackConfig = ({
|
|||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
experiments: {
|
experiments: {
|
||||||
topLevelAwait: true,
|
outputModule: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -243,4 +253,5 @@ module.exports = {
|
|||||||
createCastConfig,
|
createCastConfig,
|
||||||
createHassioConfig,
|
createHassioConfig,
|
||||||
createGalleryConfig,
|
createGalleryConfig,
|
||||||
|
createWebpackConfig,
|
||||||
};
|
};
|
||||||
|
24
gallery/src/data/date-options.ts
Normal file
24
gallery/src/data/date-options.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { ControlSelectOption } from "../../../src/components/ha-control-select";
|
||||||
|
|
||||||
|
export const timeOptions: ControlSelectOption[] = [
|
||||||
|
{
|
||||||
|
value: "now",
|
||||||
|
label: "Now",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "00:15:30",
|
||||||
|
label: "12:15:30 AM",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "06:15:30",
|
||||||
|
label: "06:15:30 AM",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "12:15:30",
|
||||||
|
label: "12:15:30 PM",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "18:15:30",
|
||||||
|
label: "06:15:30 PM",
|
||||||
|
},
|
||||||
|
];
|
@ -41,6 +41,7 @@ const triggers = [
|
|||||||
{ platform: "sun", event: "sunset" },
|
{ platform: "sun", event: "sunset" },
|
||||||
{ platform: "time_pattern" },
|
{ platform: "time_pattern" },
|
||||||
{ platform: "webhook" },
|
{ platform: "webhook" },
|
||||||
|
{ platform: "persistent_notification" },
|
||||||
{
|
{
|
||||||
platform: "zone",
|
platform: "zone",
|
||||||
entity_id: "person.person",
|
entity_id: "person.person",
|
||||||
@ -50,6 +51,11 @@ const triggers = [
|
|||||||
{ platform: "tag" },
|
{ platform: "tag" },
|
||||||
{ platform: "time", at: "15:32" },
|
{ platform: "time", at: "15:32" },
|
||||||
{ platform: "template" },
|
{ platform: "template" },
|
||||||
|
{ platform: "conversation", command: "Turn on the lights" },
|
||||||
|
{
|
||||||
|
platform: "conversation",
|
||||||
|
command: ["Turn on the lights", "Turn the lights on"],
|
||||||
|
},
|
||||||
{ platform: "event", event_type: "homeassistant_started" },
|
{ platform: "event", event_type: "homeassistant_started" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -19,11 +19,13 @@ import { HaTemplateTrigger } from "../../../../src/panels/config/automation/trig
|
|||||||
import { HaTimeTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-time";
|
import { HaTimeTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-time";
|
||||||
import { HaTimePatternTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-time_pattern";
|
import { HaTimePatternTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-time_pattern";
|
||||||
import { HaWebhookTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-webhook";
|
import { HaWebhookTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-webhook";
|
||||||
|
import { HaPersistentNotificationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-persistent_notification";
|
||||||
import { HaZoneTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-zone";
|
import { HaZoneTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-zone";
|
||||||
import { HaDeviceTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-device";
|
import { HaDeviceTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-device";
|
||||||
import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-state";
|
import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-state";
|
||||||
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
|
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
|
||||||
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
|
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
|
||||||
|
import { HaConversationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-conversation";
|
||||||
|
|
||||||
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||||
{
|
{
|
||||||
@ -72,6 +74,16 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
|||||||
triggers: [{ platform: "webhook", ...HaWebhookTrigger.defaultConfig }],
|
triggers: [{ platform: "webhook", ...HaWebhookTrigger.defaultConfig }],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Persistent Notification",
|
||||||
|
triggers: [
|
||||||
|
{
|
||||||
|
platform: "persistent_notification",
|
||||||
|
...HaPersistentNotificationTrigger.defaultConfig,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "Zone",
|
name: "Zone",
|
||||||
triggers: [{ platform: "zone", ...HaZoneTrigger.defaultConfig }],
|
triggers: [{ platform: "zone", ...HaZoneTrigger.defaultConfig }],
|
||||||
@ -101,6 +113,16 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
|||||||
name: "Device Trigger",
|
name: "Device Trigger",
|
||||||
triggers: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }],
|
triggers: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Sentence",
|
||||||
|
triggers: [
|
||||||
|
{ platform: "conversation", ...HaConversationTrigger.defaultConfig },
|
||||||
|
{
|
||||||
|
platform: "conversation",
|
||||||
|
command: ["Turn on the lights", "Turn the lights on"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@customElement("demo-automation-editor-trigger")
|
@customElement("demo-automation-editor-trigger")
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
title: Control Circular Slider
|
||||||
|
---
|
178
gallery/src/pages/components/ha-control-circular-slider.ts
Normal file
178
gallery/src/pages/components/ha-control-circular-slider.ts
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import { css, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-control-circular-slider";
|
||||||
|
import "../../../../src/components/ha-slider";
|
||||||
|
|
||||||
|
@customElement("demo-components-ha-control-circular-slider")
|
||||||
|
export class DemoHaCircularSlider extends LitElement {
|
||||||
|
@state()
|
||||||
|
private current = 22;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private low = 19;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private high = 25;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private changingLow?: number;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private changingHigh?: number;
|
||||||
|
|
||||||
|
private _lowChanged(ev) {
|
||||||
|
this.low = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _lowChanging(ev) {
|
||||||
|
this.changingLow = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _highChanged(ev) {
|
||||||
|
this.high = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _highChanging(ev) {
|
||||||
|
this.changingHigh = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _currentChanged(ev) {
|
||||||
|
this.current = ev.currentTarget.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<p class="title"><b>Config</b></p>
|
||||||
|
<div class="field">
|
||||||
|
<p>Current</p>
|
||||||
|
<ha-slider
|
||||||
|
min="10"
|
||||||
|
max="30"
|
||||||
|
.value=${this.current}
|
||||||
|
@change=${this._currentChanged}
|
||||||
|
pin
|
||||||
|
></ha-slider>
|
||||||
|
<p>${this.current} °C</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<p class="title"><b>Single</b></p>
|
||||||
|
<ha-control-circular-slider
|
||||||
|
@value-changed=${this._lowChanged}
|
||||||
|
@value-changing=${this._lowChanging}
|
||||||
|
.value=${this.low}
|
||||||
|
.current=${this.current}
|
||||||
|
step="1"
|
||||||
|
min="10"
|
||||||
|
max="30"
|
||||||
|
></ha-control-circular-slider>
|
||||||
|
<div>
|
||||||
|
Low: ${this.low} °C
|
||||||
|
<br />
|
||||||
|
Changing:
|
||||||
|
${this.changingLow != null ? `${this.changingLow} °C` : "-"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<p class="title"><b>Inverted</b></p>
|
||||||
|
<ha-control-circular-slider
|
||||||
|
inverted
|
||||||
|
@value-changed=${this._highChanged}
|
||||||
|
@value-changing=${this._highChanging}
|
||||||
|
.value=${this.high}
|
||||||
|
.current=${this.current}
|
||||||
|
step="1"
|
||||||
|
min="10"
|
||||||
|
max="30"
|
||||||
|
></ha-control-circular-slider>
|
||||||
|
<div>
|
||||||
|
High: ${this.high} °C
|
||||||
|
<br />
|
||||||
|
Changing:
|
||||||
|
${this.changingHigh != null ? `${this.changingHigh} °C` : "-"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<p class="title"><b>Dual</b></p>
|
||||||
|
<ha-control-circular-slider
|
||||||
|
dual
|
||||||
|
@low-changed=${this._lowChanged}
|
||||||
|
@low-changing=${this._lowChanging}
|
||||||
|
@high-changed=${this._highChanged}
|
||||||
|
@high-changing=${this._highChanging}
|
||||||
|
.low=${this.low}
|
||||||
|
.high=${this.high}
|
||||||
|
.current=${this.current}
|
||||||
|
step="1"
|
||||||
|
min="10"
|
||||||
|
max="30"
|
||||||
|
></ha-control-circular-slider>
|
||||||
|
<div>
|
||||||
|
Low value: ${this.low} °C
|
||||||
|
<br />
|
||||||
|
Low changing:
|
||||||
|
${this.changingLow != null ? `${this.changingLow} °C` : "-"}
|
||||||
|
<br />
|
||||||
|
High value: ${this.high} °C
|
||||||
|
<br />
|
||||||
|
High changing:
|
||||||
|
${this.changingHigh != null ? `${this.changingHigh} °C` : "-"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-card {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
p.title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
ha-control-circular-slider {
|
||||||
|
--control-circular-slider-color: #ff9800;
|
||||||
|
--control-circular-slider-background: #ff9800;
|
||||||
|
--control-circular-slider-background-opacity: 0.3;
|
||||||
|
}
|
||||||
|
ha-control-circular-slider[inverted] {
|
||||||
|
--control-circular-slider-color: #2196f3;
|
||||||
|
--control-circular-slider-background: #2196f3;
|
||||||
|
}
|
||||||
|
ha-control-circular-slider[dual] {
|
||||||
|
--control-circular-slider-high-color: #2196f3;
|
||||||
|
--control-circular-slider-low-color: #ff9800;
|
||||||
|
--control-circular-slider-background: var(--disabled-color);
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-components-ha-control-circular-slider": DemoHaCircularSlider;
|
||||||
|
}
|
||||||
|
}
|
7
gallery/src/pages/date-time/date-time-numeric.markdown
Normal file
7
gallery/src/pages/date-time/date-time-numeric.markdown
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Date-Time Format (Numeric)
|
||||||
|
---
|
||||||
|
|
||||||
|
This pages lists all supported languages with their available date-time formats.
|
||||||
|
|
||||||
|
Formatting function: `const formatDateTimeNumeric: (dateObj: Date, locale: FrontendLocaleData) => string`
|
136
gallery/src/pages/date-time/date-time-numeric.ts
Normal file
136
gallery/src/pages/date-time/date-time-numeric.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { html, css, LitElement } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-control-select";
|
||||||
|
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||||
|
import { formatDateTimeNumeric } from "../../../../src/common/datetime/format_date_time";
|
||||||
|
import { timeOptions } from "../../data/date-options";
|
||||||
|
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||||
|
import {
|
||||||
|
FrontendLocaleData,
|
||||||
|
NumberFormat,
|
||||||
|
TimeFormat,
|
||||||
|
DateFormat,
|
||||||
|
FirstWeekday,
|
||||||
|
TimeZone,
|
||||||
|
} from "../../../../src/data/translation";
|
||||||
|
|
||||||
|
@customElement("demo-date-time-date-time-numeric")
|
||||||
|
export class DemoDateTimeDateTimeNumeric extends LitElement {
|
||||||
|
@state() private selection?: string = "now";
|
||||||
|
|
||||||
|
@state() private date: Date = new Date();
|
||||||
|
|
||||||
|
handleValueChanged(e: CustomEvent) {
|
||||||
|
this.selection = e.detail.value as string;
|
||||||
|
this.date = new Date();
|
||||||
|
if (this.selection !== "now") {
|
||||||
|
const [hours, minutes, seconds] = this.selection.split(":").map(Number);
|
||||||
|
this.date.setHours(hours);
|
||||||
|
this.date.setMinutes(minutes);
|
||||||
|
this.date.setSeconds(seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const defaultLocale: FrontendLocaleData = {
|
||||||
|
language: "en",
|
||||||
|
number_format: NumberFormat.language,
|
||||||
|
time_format: TimeFormat.language,
|
||||||
|
date_format: DateFormat.language,
|
||||||
|
first_weekday: FirstWeekday.language,
|
||||||
|
time_zone: TimeZone.local,
|
||||||
|
};
|
||||||
|
return html`
|
||||||
|
<ha-control-select
|
||||||
|
.value=${this.selection}
|
||||||
|
.options=${timeOptions}
|
||||||
|
@value-changed=${this.handleValueChanged}
|
||||||
|
>
|
||||||
|
</ha-control-select>
|
||||||
|
<mwc-list>
|
||||||
|
<div class="container header">
|
||||||
|
<div>Language</div>
|
||||||
|
<div class="center">Default (lang)</div>
|
||||||
|
<div class="center">12 Hours</div>
|
||||||
|
<div class="center">24 Hours</div>
|
||||||
|
</div>
|
||||||
|
${Object.entries(translationMetadata.translations)
|
||||||
|
.filter(([key, _]) => key !== "test")
|
||||||
|
.map(
|
||||||
|
([key, value]) => html`
|
||||||
|
<div class="container">
|
||||||
|
<div>${value.nativeName}</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateTimeNumeric(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.language,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateTimeNumeric(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.am_pm,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateTimeNumeric(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.twenty_four,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-control-select {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 12px auto;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 12px auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > div {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-date-time-date-time-numeric": DemoDateTimeDateTimeNumeric;
|
||||||
|
}
|
||||||
|
}
|
7
gallery/src/pages/date-time/date-time-seconds.markdown
Normal file
7
gallery/src/pages/date-time/date-time-seconds.markdown
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Date-Time Format (Seconds)
|
||||||
|
---
|
||||||
|
|
||||||
|
This pages lists all supported languages with their available date-time formats.
|
||||||
|
|
||||||
|
Formatting function: `const formatDateTimeWithSeconds: (dateObj: Date, locale: FrontendLocaleData) => string`
|
136
gallery/src/pages/date-time/date-time-seconds.ts
Normal file
136
gallery/src/pages/date-time/date-time-seconds.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { html, css, LitElement } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-control-select";
|
||||||
|
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||||
|
import { formatDateTimeWithSeconds } from "../../../../src/common/datetime/format_date_time";
|
||||||
|
import { timeOptions } from "../../data/date-options";
|
||||||
|
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||||
|
import {
|
||||||
|
FrontendLocaleData,
|
||||||
|
NumberFormat,
|
||||||
|
TimeFormat,
|
||||||
|
DateFormat,
|
||||||
|
FirstWeekday,
|
||||||
|
TimeZone,
|
||||||
|
} from "../../../../src/data/translation";
|
||||||
|
|
||||||
|
@customElement("demo-date-time-date-time-seconds")
|
||||||
|
export class DemoDateTimeDateTimeSeconds extends LitElement {
|
||||||
|
@state() private selection?: string = "now";
|
||||||
|
|
||||||
|
@state() private date: Date = new Date();
|
||||||
|
|
||||||
|
handleValueChanged(e: CustomEvent) {
|
||||||
|
this.selection = e.detail.value as string;
|
||||||
|
this.date = new Date();
|
||||||
|
if (this.selection !== "now") {
|
||||||
|
const [hours, minutes, seconds] = this.selection.split(":").map(Number);
|
||||||
|
this.date.setHours(hours);
|
||||||
|
this.date.setMinutes(minutes);
|
||||||
|
this.date.setSeconds(seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const defaultLocale: FrontendLocaleData = {
|
||||||
|
language: "en",
|
||||||
|
number_format: NumberFormat.language,
|
||||||
|
time_format: TimeFormat.language,
|
||||||
|
date_format: DateFormat.language,
|
||||||
|
first_weekday: FirstWeekday.language,
|
||||||
|
time_zone: TimeZone.local,
|
||||||
|
};
|
||||||
|
return html`
|
||||||
|
<ha-control-select
|
||||||
|
.value=${this.selection}
|
||||||
|
.options=${timeOptions}
|
||||||
|
@value-changed=${this.handleValueChanged}
|
||||||
|
>
|
||||||
|
</ha-control-select>
|
||||||
|
<mwc-list>
|
||||||
|
<div class="container header">
|
||||||
|
<div>Language</div>
|
||||||
|
<div class="center">Default (lang)</div>
|
||||||
|
<div class="center">12 Hours</div>
|
||||||
|
<div class="center">24 Hours</div>
|
||||||
|
</div>
|
||||||
|
${Object.entries(translationMetadata.translations)
|
||||||
|
.filter(([key, _]) => key !== "test")
|
||||||
|
.map(
|
||||||
|
([key, value]) => html`
|
||||||
|
<div class="container">
|
||||||
|
<div>${value.nativeName}</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateTimeWithSeconds(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.language,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateTimeWithSeconds(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.am_pm,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateTimeWithSeconds(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.twenty_four,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-control-select {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 12px auto;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 12px auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > div {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-date-time-date-time-seconds": DemoDateTimeDateTimeSeconds;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Date-Time Format (Short w/ Year)
|
||||||
|
---
|
||||||
|
|
||||||
|
This pages lists all supported languages with their available date-time formats.
|
||||||
|
|
||||||
|
Formatting function: `const formatShortDateTimeWithYear: (dateObj: Date, locale: FrontendLocaleData) => string`
|
136
gallery/src/pages/date-time/date-time-short-year.ts
Normal file
136
gallery/src/pages/date-time/date-time-short-year.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { html, css, LitElement } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-control-select";
|
||||||
|
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||||
|
import { formatShortDateTimeWithYear } from "../../../../src/common/datetime/format_date_time";
|
||||||
|
import { timeOptions } from "../../data/date-options";
|
||||||
|
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||||
|
import {
|
||||||
|
FrontendLocaleData,
|
||||||
|
NumberFormat,
|
||||||
|
TimeFormat,
|
||||||
|
DateFormat,
|
||||||
|
FirstWeekday,
|
||||||
|
TimeZone,
|
||||||
|
} from "../../../../src/data/translation";
|
||||||
|
|
||||||
|
@customElement("demo-date-time-date-time-short-year")
|
||||||
|
export class DemoDateTimeDateTimeShortYear extends LitElement {
|
||||||
|
@state() private selection?: string = "now";
|
||||||
|
|
||||||
|
@state() private date: Date = new Date();
|
||||||
|
|
||||||
|
handleValueChanged(e: CustomEvent) {
|
||||||
|
this.selection = e.detail.value as string;
|
||||||
|
this.date = new Date();
|
||||||
|
if (this.selection !== "now") {
|
||||||
|
const [hours, minutes, seconds] = this.selection.split(":").map(Number);
|
||||||
|
this.date.setHours(hours);
|
||||||
|
this.date.setMinutes(minutes);
|
||||||
|
this.date.setSeconds(seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const defaultLocale: FrontendLocaleData = {
|
||||||
|
language: "en",
|
||||||
|
number_format: NumberFormat.language,
|
||||||
|
time_format: TimeFormat.language,
|
||||||
|
date_format: DateFormat.language,
|
||||||
|
first_weekday: FirstWeekday.language,
|
||||||
|
time_zone: TimeZone.local,
|
||||||
|
};
|
||||||
|
return html`
|
||||||
|
<ha-control-select
|
||||||
|
.value=${this.selection}
|
||||||
|
.options=${timeOptions}
|
||||||
|
@value-changed=${this.handleValueChanged}
|
||||||
|
>
|
||||||
|
</ha-control-select>
|
||||||
|
<mwc-list>
|
||||||
|
<div class="container header">
|
||||||
|
<div>Language</div>
|
||||||
|
<div class="center">Default (lang)</div>
|
||||||
|
<div class="center">12 Hours</div>
|
||||||
|
<div class="center">24 Hours</div>
|
||||||
|
</div>
|
||||||
|
${Object.entries(translationMetadata.translations)
|
||||||
|
.filter(([key, _]) => key !== "test")
|
||||||
|
.map(
|
||||||
|
([key, value]) => html`
|
||||||
|
<div class="container">
|
||||||
|
<div>${value.nativeName}</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatShortDateTimeWithYear(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.language,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatShortDateTimeWithYear(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.am_pm,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatShortDateTimeWithYear(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.twenty_four,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-control-select {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 12px auto;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 12px auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > div {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-date-time-date-time-short-year": DemoDateTimeDateTimeShortYear;
|
||||||
|
}
|
||||||
|
}
|
7
gallery/src/pages/date-time/date-time-short.markdown
Normal file
7
gallery/src/pages/date-time/date-time-short.markdown
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Date-Time Format (Short)
|
||||||
|
---
|
||||||
|
|
||||||
|
This pages lists all supported languages with their available date-time formats.
|
||||||
|
|
||||||
|
Formatting function: `const formatShortDateTime: (dateObj: Date, locale: FrontendLocaleData) => string`
|
136
gallery/src/pages/date-time/date-time-short.ts
Normal file
136
gallery/src/pages/date-time/date-time-short.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { html, css, LitElement } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-control-select";
|
||||||
|
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||||
|
import { formatShortDateTime } from "../../../../src/common/datetime/format_date_time";
|
||||||
|
import { timeOptions } from "../../data/date-options";
|
||||||
|
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||||
|
import {
|
||||||
|
FrontendLocaleData,
|
||||||
|
NumberFormat,
|
||||||
|
TimeFormat,
|
||||||
|
DateFormat,
|
||||||
|
FirstWeekday,
|
||||||
|
TimeZone,
|
||||||
|
} from "../../../../src/data/translation";
|
||||||
|
|
||||||
|
@customElement("demo-date-time-date-time-short")
|
||||||
|
export class DemoDateTimeDateTimeShort extends LitElement {
|
||||||
|
@state() private selection?: string = "now";
|
||||||
|
|
||||||
|
@state() private date: Date = new Date();
|
||||||
|
|
||||||
|
handleValueChanged(e: CustomEvent) {
|
||||||
|
this.selection = e.detail.value as string;
|
||||||
|
this.date = new Date();
|
||||||
|
if (this.selection !== "now") {
|
||||||
|
const [hours, minutes, seconds] = this.selection.split(":").map(Number);
|
||||||
|
this.date.setHours(hours);
|
||||||
|
this.date.setMinutes(minutes);
|
||||||
|
this.date.setSeconds(seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const defaultLocale: FrontendLocaleData = {
|
||||||
|
language: "en",
|
||||||
|
number_format: NumberFormat.language,
|
||||||
|
time_format: TimeFormat.language,
|
||||||
|
date_format: DateFormat.language,
|
||||||
|
first_weekday: FirstWeekday.language,
|
||||||
|
time_zone: TimeZone.local,
|
||||||
|
};
|
||||||
|
return html`
|
||||||
|
<ha-control-select
|
||||||
|
.value=${this.selection}
|
||||||
|
.options=${timeOptions}
|
||||||
|
@value-changed=${this.handleValueChanged}
|
||||||
|
>
|
||||||
|
</ha-control-select>
|
||||||
|
<mwc-list>
|
||||||
|
<div class="container header">
|
||||||
|
<div>Language</div>
|
||||||
|
<div class="center">Default (lang)</div>
|
||||||
|
<div class="center">12 Hours</div>
|
||||||
|
<div class="center">24 Hours</div>
|
||||||
|
</div>
|
||||||
|
${Object.entries(translationMetadata.translations)
|
||||||
|
.filter(([key, _]) => key !== "test")
|
||||||
|
.map(
|
||||||
|
([key, value]) => html`
|
||||||
|
<div class="container">
|
||||||
|
<div>${value.nativeName}</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatShortDateTime(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.language,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatShortDateTime(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.am_pm,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatShortDateTime(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.twenty_four,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-control-select {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 12px auto;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 12px auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > div {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-date-time-date-time-short": DemoDateTimeDateTimeShort;
|
||||||
|
}
|
||||||
|
}
|
7
gallery/src/pages/date-time/date-time.markdown
Normal file
7
gallery/src/pages/date-time/date-time.markdown
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Date-Time Format
|
||||||
|
---
|
||||||
|
|
||||||
|
This pages lists all supported languages with their available date-time formats.
|
||||||
|
|
||||||
|
Formatting function: `const formatDateTime: (dateObj: Date, locale: FrontendLocaleData) => string`
|
136
gallery/src/pages/date-time/date-time.ts
Normal file
136
gallery/src/pages/date-time/date-time.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { html, css, LitElement } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-control-select";
|
||||||
|
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||||
|
import { formatDateTime } from "../../../../src/common/datetime/format_date_time";
|
||||||
|
import { timeOptions } from "../../data/date-options";
|
||||||
|
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||||
|
import {
|
||||||
|
FrontendLocaleData,
|
||||||
|
NumberFormat,
|
||||||
|
TimeFormat,
|
||||||
|
DateFormat,
|
||||||
|
FirstWeekday,
|
||||||
|
TimeZone,
|
||||||
|
} from "../../../../src/data/translation";
|
||||||
|
|
||||||
|
@customElement("demo-date-time-date-time")
|
||||||
|
export class DemoDateTimeDateTime extends LitElement {
|
||||||
|
@state() private selection?: string = "now";
|
||||||
|
|
||||||
|
@state() private date: Date = new Date();
|
||||||
|
|
||||||
|
handleValueChanged(e: CustomEvent) {
|
||||||
|
this.selection = e.detail.value as string;
|
||||||
|
this.date = new Date();
|
||||||
|
if (this.selection !== "now") {
|
||||||
|
const [hours, minutes, seconds] = this.selection.split(":").map(Number);
|
||||||
|
this.date.setHours(hours);
|
||||||
|
this.date.setMinutes(minutes);
|
||||||
|
this.date.setSeconds(seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const defaultLocale: FrontendLocaleData = {
|
||||||
|
language: "en",
|
||||||
|
number_format: NumberFormat.language,
|
||||||
|
time_format: TimeFormat.language,
|
||||||
|
date_format: DateFormat.language,
|
||||||
|
first_weekday: FirstWeekday.language,
|
||||||
|
time_zone: TimeZone.local,
|
||||||
|
};
|
||||||
|
return html`
|
||||||
|
<ha-control-select
|
||||||
|
.value=${this.selection}
|
||||||
|
.options=${timeOptions}
|
||||||
|
@value-changed=${this.handleValueChanged}
|
||||||
|
>
|
||||||
|
</ha-control-select>
|
||||||
|
<mwc-list>
|
||||||
|
<div class="container header">
|
||||||
|
<div>Language</div>
|
||||||
|
<div class="center">Default (lang)</div>
|
||||||
|
<div class="center">12 Hours</div>
|
||||||
|
<div class="center">24 Hours</div>
|
||||||
|
</div>
|
||||||
|
${Object.entries(translationMetadata.translations)
|
||||||
|
.filter(([key, _]) => key !== "test")
|
||||||
|
.map(
|
||||||
|
([key, value]) => html`
|
||||||
|
<div class="container">
|
||||||
|
<div>${value.nativeName}</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateTime(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.language,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateTime(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.am_pm,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateTime(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.twenty_four,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-control-select {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 12px auto;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 12px auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > div {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-date-time-date-time": DemoDateTimeDateTime;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: (Numeric) Date Formatting
|
title: Date Format (Numeric)
|
||||||
---
|
---
|
||||||
|
|
||||||
This pages lists all supported languages with their available (numeric) date formats.
|
This pages lists all supported languages with their available (numeric) date formats.
|
||||||
|
@ -1,27 +1,28 @@
|
|||||||
import { html, css, LitElement } from "lit";
|
import "@material/mwc-list/mwc-list";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { css, html, LitElement } from "lit";
|
||||||
import "../../../../src/components/ha-card";
|
import { customElement } from "lit/decorators";
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
|
||||||
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
|
||||||
import { formatDateNumeric } from "../../../../src/common/datetime/format_date";
|
import { formatDateNumeric } from "../../../../src/common/datetime/format_date";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
import {
|
import {
|
||||||
|
DateFormat,
|
||||||
|
FirstWeekday,
|
||||||
FrontendLocaleData,
|
FrontendLocaleData,
|
||||||
NumberFormat,
|
NumberFormat,
|
||||||
TimeFormat,
|
TimeFormat,
|
||||||
DateFormat,
|
TimeZone,
|
||||||
FirstWeekday,
|
|
||||||
} from "../../../../src/data/translation";
|
} from "../../../../src/data/translation";
|
||||||
|
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||||
|
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||||
|
|
||||||
@customElement("demo-date-time-date")
|
@customElement("demo-date-time-date")
|
||||||
export class DemoDateTimeDate extends LitElement {
|
export class DemoDateTimeDate extends LitElement {
|
||||||
@property({ attribute: false }) hass!: HomeAssistant;
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const defaultLocale: FrontendLocaleData = {
|
const defaultLocale: FrontendLocaleData = {
|
||||||
language: "en",
|
language: "en",
|
||||||
number_format: NumberFormat.language,
|
number_format: NumberFormat.language,
|
||||||
time_format: TimeFormat.language,
|
time_format: TimeFormat.language,
|
||||||
date_format: DateFormat.language,
|
date_format: DateFormat.language,
|
||||||
|
time_zone: TimeZone.local,
|
||||||
first_weekday: FirstWeekday.language,
|
first_weekday: FirstWeekday.language,
|
||||||
};
|
};
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
@ -41,32 +42,48 @@ export class DemoDateTimeDate extends LitElement {
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div>${value.nativeName}</div>
|
<div>${value.nativeName}</div>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
${formatDateNumeric(date, {
|
${formatDateNumeric(
|
||||||
|
date,
|
||||||
|
{
|
||||||
...defaultLocale,
|
...defaultLocale,
|
||||||
language: key,
|
language: key,
|
||||||
date_format: DateFormat.language,
|
date_format: DateFormat.language,
|
||||||
})}
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
${formatDateNumeric(date, {
|
${formatDateNumeric(
|
||||||
|
date,
|
||||||
|
{
|
||||||
...defaultLocale,
|
...defaultLocale,
|
||||||
language: key,
|
language: key,
|
||||||
date_format: DateFormat.DMY,
|
date_format: DateFormat.DMY,
|
||||||
})}
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
${formatDateNumeric(date, {
|
${formatDateNumeric(
|
||||||
|
date,
|
||||||
|
{
|
||||||
...defaultLocale,
|
...defaultLocale,
|
||||||
language: key,
|
language: key,
|
||||||
date_format: DateFormat.MDY,
|
date_format: DateFormat.MDY,
|
||||||
})}
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
${formatDateNumeric(date, {
|
${formatDateNumeric(
|
||||||
|
date,
|
||||||
|
{
|
||||||
...defaultLocale,
|
...defaultLocale,
|
||||||
language: key,
|
language: key,
|
||||||
date_format: DateFormat.YMD,
|
date_format: DateFormat.YMD,
|
||||||
})}
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
7
gallery/src/pages/date-time/time-seconds.markdown
Normal file
7
gallery/src/pages/date-time/time-seconds.markdown
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Time Format (Seconds)
|
||||||
|
---
|
||||||
|
|
||||||
|
This pages lists all supported languages with their available time formats.
|
||||||
|
|
||||||
|
Formatting function: `const formatTimeWithSeconds: (dateObj: Date, locale: FrontendLocaleData) => string`
|
135
gallery/src/pages/date-time/time-seconds.ts
Normal file
135
gallery/src/pages/date-time/time-seconds.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { html, css, LitElement } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||||
|
import { formatTimeWithSeconds } from "../../../../src/common/datetime/format_time";
|
||||||
|
import { timeOptions } from "../../data/date-options";
|
||||||
|
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||||
|
import {
|
||||||
|
FrontendLocaleData,
|
||||||
|
NumberFormat,
|
||||||
|
TimeFormat,
|
||||||
|
DateFormat,
|
||||||
|
FirstWeekday,
|
||||||
|
TimeZone,
|
||||||
|
} from "../../../../src/data/translation";
|
||||||
|
|
||||||
|
@customElement("demo-date-time-time-seconds")
|
||||||
|
export class DemoDateTimeTimeSeconds extends LitElement {
|
||||||
|
@state() private selection?: string = "now";
|
||||||
|
|
||||||
|
@state() private date: Date = new Date();
|
||||||
|
|
||||||
|
handleValueChanged(e: CustomEvent) {
|
||||||
|
this.selection = e.detail.value as string;
|
||||||
|
this.date = new Date();
|
||||||
|
if (this.selection !== "now") {
|
||||||
|
const [hours, minutes, seconds] = this.selection.split(":").map(Number);
|
||||||
|
this.date.setHours(hours);
|
||||||
|
this.date.setMinutes(minutes);
|
||||||
|
this.date.setSeconds(seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const defaultLocale: FrontendLocaleData = {
|
||||||
|
language: "en",
|
||||||
|
number_format: NumberFormat.language,
|
||||||
|
time_format: TimeFormat.language,
|
||||||
|
date_format: DateFormat.language,
|
||||||
|
first_weekday: FirstWeekday.language,
|
||||||
|
time_zone: TimeZone.local,
|
||||||
|
};
|
||||||
|
return html`
|
||||||
|
<ha-control-select
|
||||||
|
.value=${this.selection}
|
||||||
|
.options=${timeOptions}
|
||||||
|
@value-changed=${this.handleValueChanged}
|
||||||
|
>
|
||||||
|
</ha-control-select>
|
||||||
|
<mwc-list>
|
||||||
|
<div class="container header">
|
||||||
|
<div>Language</div>
|
||||||
|
<div class="center">Default (lang)</div>
|
||||||
|
<div class="center">12 Hours</div>
|
||||||
|
<div class="center">24 Hours</div>
|
||||||
|
</div>
|
||||||
|
${Object.entries(translationMetadata.translations)
|
||||||
|
.filter(([key, _]) => key !== "test")
|
||||||
|
.map(
|
||||||
|
([key, value]) => html`
|
||||||
|
<div class="container">
|
||||||
|
<div>${value.nativeName}</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatTimeWithSeconds(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.language,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatTimeWithSeconds(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.am_pm,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatTimeWithSeconds(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.twenty_four,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-control-select {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 12px auto;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 12px auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > div {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-date-time-time-seconds": DemoDateTimeTimeSeconds;
|
||||||
|
}
|
||||||
|
}
|
7
gallery/src/pages/date-time/time-weekday.markdown
Normal file
7
gallery/src/pages/date-time/time-weekday.markdown
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Time Format (Weekday)
|
||||||
|
---
|
||||||
|
|
||||||
|
This pages lists all supported languages with their available time formats.
|
||||||
|
|
||||||
|
Formatting function: `const formatTimeWeekday: (dateObj: Date, locale: FrontendLocaleData) => string`
|
135
gallery/src/pages/date-time/time-weekday.ts
Normal file
135
gallery/src/pages/date-time/time-weekday.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { html, css, LitElement } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||||
|
import { formatTimeWeekday } from "../../../../src/common/datetime/format_time";
|
||||||
|
import { timeOptions } from "../../data/date-options";
|
||||||
|
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||||
|
import {
|
||||||
|
FrontendLocaleData,
|
||||||
|
NumberFormat,
|
||||||
|
TimeFormat,
|
||||||
|
DateFormat,
|
||||||
|
FirstWeekday,
|
||||||
|
TimeZone,
|
||||||
|
} from "../../../../src/data/translation";
|
||||||
|
|
||||||
|
@customElement("demo-date-time-time-weekday")
|
||||||
|
export class DemoDateTimeTimeWeekday extends LitElement {
|
||||||
|
@state() private selection?: string = "now";
|
||||||
|
|
||||||
|
@state() private date: Date = new Date();
|
||||||
|
|
||||||
|
handleValueChanged(e: CustomEvent) {
|
||||||
|
this.selection = e.detail.value as string;
|
||||||
|
this.date = new Date();
|
||||||
|
if (this.selection !== "now") {
|
||||||
|
const [hours, minutes, seconds] = this.selection.split(":").map(Number);
|
||||||
|
this.date.setHours(hours);
|
||||||
|
this.date.setMinutes(minutes);
|
||||||
|
this.date.setSeconds(seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const defaultLocale: FrontendLocaleData = {
|
||||||
|
language: "en",
|
||||||
|
number_format: NumberFormat.language,
|
||||||
|
time_format: TimeFormat.language,
|
||||||
|
date_format: DateFormat.language,
|
||||||
|
first_weekday: FirstWeekday.language,
|
||||||
|
time_zone: TimeZone.local,
|
||||||
|
};
|
||||||
|
return html`
|
||||||
|
<ha-control-select
|
||||||
|
.value=${this.selection}
|
||||||
|
.options=${timeOptions}
|
||||||
|
@value-changed=${this.handleValueChanged}
|
||||||
|
>
|
||||||
|
</ha-control-select>
|
||||||
|
<mwc-list>
|
||||||
|
<div class="container header">
|
||||||
|
<div>Language</div>
|
||||||
|
<div class="center">Default (lang)</div>
|
||||||
|
<div class="center">12 Hours</div>
|
||||||
|
<div class="center">24 Hours</div>
|
||||||
|
</div>
|
||||||
|
${Object.entries(translationMetadata.translations)
|
||||||
|
.filter(([key, _]) => key !== "test")
|
||||||
|
.map(
|
||||||
|
([key, value]) => html`
|
||||||
|
<div class="container">
|
||||||
|
<div>${value.nativeName}</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatTimeWeekday(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.language,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatTimeWeekday(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.am_pm,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatTimeWeekday(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.twenty_four,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-control-select {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 12px auto;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 12px auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > div {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-date-time-time-weekday": DemoDateTimeTimeWeekday;
|
||||||
|
}
|
||||||
|
}
|
7
gallery/src/pages/date-time/time.markdown
Normal file
7
gallery/src/pages/date-time/time.markdown
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Time Format
|
||||||
|
---
|
||||||
|
|
||||||
|
This pages lists all supported languages with their available time formats.
|
||||||
|
|
||||||
|
Formatting function: `const formatTime: (dateObj: Date, locale: FrontendLocaleData) => string`
|
136
gallery/src/pages/date-time/time.ts
Normal file
136
gallery/src/pages/date-time/time.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { html, css, LitElement } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-control-select";
|
||||||
|
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||||
|
import { formatTime } from "../../../../src/common/datetime/format_time";
|
||||||
|
import { timeOptions } from "../../data/date-options";
|
||||||
|
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||||
|
import {
|
||||||
|
FrontendLocaleData,
|
||||||
|
NumberFormat,
|
||||||
|
TimeFormat,
|
||||||
|
DateFormat,
|
||||||
|
FirstWeekday,
|
||||||
|
TimeZone,
|
||||||
|
} from "../../../../src/data/translation";
|
||||||
|
|
||||||
|
@customElement("demo-date-time-time")
|
||||||
|
export class DemoDateTimeTime extends LitElement {
|
||||||
|
@state() private selection?: string = "now";
|
||||||
|
|
||||||
|
@state() private date: Date = new Date();
|
||||||
|
|
||||||
|
handleValueChanged(e: CustomEvent) {
|
||||||
|
this.selection = e.detail.value as string;
|
||||||
|
this.date = new Date();
|
||||||
|
if (this.selection !== "now") {
|
||||||
|
const [hours, minutes, seconds] = this.selection.split(":").map(Number);
|
||||||
|
this.date.setHours(hours);
|
||||||
|
this.date.setMinutes(minutes);
|
||||||
|
this.date.setSeconds(seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const defaultLocale: FrontendLocaleData = {
|
||||||
|
language: "en",
|
||||||
|
number_format: NumberFormat.language,
|
||||||
|
time_format: TimeFormat.language,
|
||||||
|
date_format: DateFormat.language,
|
||||||
|
first_weekday: FirstWeekday.language,
|
||||||
|
time_zone: TimeZone.local,
|
||||||
|
};
|
||||||
|
return html`
|
||||||
|
<ha-control-select
|
||||||
|
.value=${this.selection}
|
||||||
|
.options=${timeOptions}
|
||||||
|
@value-changed=${this.handleValueChanged}
|
||||||
|
>
|
||||||
|
</ha-control-select>
|
||||||
|
<mwc-list>
|
||||||
|
<div class="container header">
|
||||||
|
<div>Language</div>
|
||||||
|
<div class="center">Default (lang)</div>
|
||||||
|
<div class="center">12 Hours</div>
|
||||||
|
<div class="center">24 Hours</div>
|
||||||
|
</div>
|
||||||
|
${Object.entries(translationMetadata.translations)
|
||||||
|
.filter(([key, _]) => key !== "test")
|
||||||
|
.map(
|
||||||
|
([key, value]) => html`
|
||||||
|
<div class="container">
|
||||||
|
<div>${value.nativeName}</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatTime(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.language,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatTime(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.am_pm,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatTime(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.twenty_four,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-control-select {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 12px auto;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 12px auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > div {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-date-time-time": DemoDateTimeTime;
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,7 @@ const CONFIGS = [
|
|||||||
heading: "markdown-it demo",
|
heading: "markdown-it demo",
|
||||||
config: `
|
config: `
|
||||||
- type: markdown
|
- type: markdown
|
||||||
content: >-
|
content: |
|
||||||
# h1 Heading 8-)
|
# h1 Heading 8-)
|
||||||
|
|
||||||
## h2 Heading
|
## h2 Heading
|
||||||
@ -65,6 +65,15 @@ const CONFIGS = [
|
|||||||
>> ...by using additional greater-than signs right next to each other...
|
>> ...by using additional greater-than signs right next to each other...
|
||||||
> > > ...or with spaces between arrows.
|
> > > ...or with spaces between arrows.
|
||||||
|
|
||||||
|
> **Warning** Hey there
|
||||||
|
> This is a warning with a title
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
> This is a note
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
> This is a multiline note
|
||||||
|
> Lorem ipsum...
|
||||||
|
|
||||||
## Lists
|
## Lists
|
||||||
|
|
||||||
|
@ -135,6 +135,9 @@ const ENTITIES: HassEntity[] = [
|
|||||||
createEntity("climate.fan_only", "fan_only"),
|
createEntity("climate.fan_only", "fan_only"),
|
||||||
createEntity("climate.auto_idle", "auto", undefined, { hvac_action: "idle" }),
|
createEntity("climate.auto_idle", "auto", undefined, { hvac_action: "idle" }),
|
||||||
createEntity("climate.auto_off", "auto", undefined, { hvac_action: "off" }),
|
createEntity("climate.auto_off", "auto", undefined, { hvac_action: "off" }),
|
||||||
|
createEntity("climate.auto_preheating", "auto", undefined, {
|
||||||
|
hvac_action: "preheating",
|
||||||
|
}),
|
||||||
createEntity("climate.auto_heating", "auto", undefined, {
|
createEntity("climate.auto_heating", "auto", undefined, {
|
||||||
hvac_action: "heating",
|
hvac_action: "heating",
|
||||||
}),
|
}),
|
||||||
@ -354,6 +357,7 @@ export class DemoEntityState extends LitElement {
|
|||||||
hass.localize,
|
hass.localize,
|
||||||
entry.stateObj,
|
entry.stateObj,
|
||||||
hass.locale,
|
hass.locale,
|
||||||
|
hass.config,
|
||||||
hass.entities
|
hass.entities
|
||||||
)}`,
|
)}`,
|
||||||
},
|
},
|
||||||
|
@ -114,11 +114,22 @@ class HassioAddonInfo extends LitElement {
|
|||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
private _fetchDataTimeout?: number;
|
||||||
|
|
||||||
private _addonStoreInfo = memoizeOne(
|
private _addonStoreInfo = memoizeOne(
|
||||||
(slug: string, storeAddons: StoreAddon[]) =>
|
(slug: string, storeAddons: StoreAddon[]) =>
|
||||||
storeAddons.find((addon) => addon.slug === slug)
|
storeAddons.find((addon) => addon.slug === slug)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
|
||||||
|
if (this._fetchDataTimeout) {
|
||||||
|
clearInterval(this._fetchDataTimeout);
|
||||||
|
this._fetchDataTimeout = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const addonStoreInfo =
|
const addonStoreInfo =
|
||||||
!this.addon.detached && !this.addon.available
|
!this.addon.detached && !this.addon.available
|
||||||
@ -592,7 +603,10 @@ class HassioAddonInfo extends LitElement {
|
|||||||
</ha-progress-button>
|
</ha-progress-button>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<ha-progress-button @click=${this._startClicked}>
|
<ha-progress-button
|
||||||
|
@click=${this._startClicked}
|
||||||
|
.progress=${this.addon.state === "startup"}
|
||||||
|
>
|
||||||
${this.supervisor.localize("addon.dashboard.start")}
|
${this.supervisor.localize("addon.dashboard.start")}
|
||||||
</ha-progress-button>
|
</ha-progress-button>
|
||||||
`
|
`
|
||||||
@ -672,8 +686,35 @@ class HassioAddonInfo extends LitElement {
|
|||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
if (changedProps.has("addon")) {
|
if (changedProps.has("addon")) {
|
||||||
this._loadData();
|
this._loadData();
|
||||||
|
if (
|
||||||
|
!this._fetchDataTimeout &&
|
||||||
|
this.addon &&
|
||||||
|
"state" in this.addon &&
|
||||||
|
this.addon.state === "startup"
|
||||||
|
) {
|
||||||
|
// Addon is starting up, wait for it to start
|
||||||
|
this._scheduleDataUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _scheduleDataUpdate() {
|
||||||
|
this._fetchDataTimeout = window.setTimeout(async () => {
|
||||||
|
const addon = await fetchHassioAddonInfo(this.hass, this.addon.slug);
|
||||||
|
if (addon.state !== "startup") {
|
||||||
|
this._fetchDataTimeout = undefined;
|
||||||
|
this.addon = addon;
|
||||||
|
const eventdata = {
|
||||||
|
success: true,
|
||||||
|
response: undefined,
|
||||||
|
path: "start",
|
||||||
|
};
|
||||||
|
fireEvent(this, "hass-api-called", eventdata);
|
||||||
|
} else {
|
||||||
|
this._scheduleDataUpdate();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
private async _loadData(): Promise<void> {
|
private async _loadData(): Promise<void> {
|
||||||
if ("state" in this.addon && this.addon.state === "started") {
|
if ("state" in this.addon && this.addon.state === "started") {
|
||||||
|
@ -136,6 +136,15 @@ export class HassioBackups extends LitElement {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
template: (entry: number) => Math.ceil(entry * 10) / 10 + " MB",
|
template: (entry: number) => Math.ceil(entry * 10) / 10 + " MB",
|
||||||
},
|
},
|
||||||
|
location: {
|
||||||
|
title: this.supervisor.localize("backup.location"),
|
||||||
|
width: "15%",
|
||||||
|
hidden: narrow,
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
template: (entry: string | null) =>
|
||||||
|
entry || this.supervisor.localize("backup.data_disk"),
|
||||||
|
},
|
||||||
date: {
|
date: {
|
||||||
title: this.supervisor.localize("backup.created"),
|
title: this.supervisor.localize("backup.created"),
|
||||||
width: "15%",
|
width: "15%",
|
||||||
|
@ -143,7 +143,11 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
: this._localize("partial_backup")}
|
: this._localize("partial_backup")}
|
||||||
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
|
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
|
||||||
${this.hass
|
${this.hass
|
||||||
? formatDateTime(new Date(this.backup.date), this.hass.locale)
|
? formatDateTime(
|
||||||
|
new Date(this.backup.date),
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config
|
||||||
|
)
|
||||||
: this.backup.date}
|
: this.backup.date}
|
||||||
</div>`
|
</div>`
|
||||||
: html`<paper-input
|
: html`<paper-input
|
||||||
@ -336,7 +340,9 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
const data: any = {};
|
const data: any = {};
|
||||||
|
|
||||||
if (!this.backup) {
|
if (!this.backup) {
|
||||||
data.name = this.backupName || formatDate(new Date(), this.hass.locale);
|
data.name =
|
||||||
|
this.backupName ||
|
||||||
|
formatDate(new Date(), this.hass.locale, this.hass.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.backupHasPassword) {
|
if (this.backupHasPassword) {
|
||||||
|
@ -137,6 +137,9 @@ class HassioAddons extends LitElement {
|
|||||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||||
}
|
}
|
||||||
|
.content {
|
||||||
|
margin-bottom: 72px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import "../../../src/components/ha-icon-button";
|
|||||||
import {
|
import {
|
||||||
fetchHassioAddonInfo,
|
fetchHassioAddonInfo,
|
||||||
HassioAddonDetails,
|
HassioAddonDetails,
|
||||||
|
startHassioAddon,
|
||||||
} from "../../../src/data/hassio/addon";
|
} from "../../../src/data/hassio/addon";
|
||||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||||
import {
|
import {
|
||||||
@ -23,7 +24,10 @@ import {
|
|||||||
validateHassioSession,
|
validateHassioSession,
|
||||||
} from "../../../src/data/hassio/ingress";
|
} from "../../../src/data/hassio/ingress";
|
||||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
import {
|
||||||
|
showAlertDialog,
|
||||||
|
showConfirmationDialog,
|
||||||
|
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||||
import "../../../src/layouts/hass-loading-screen";
|
import "../../../src/layouts/hass-loading-screen";
|
||||||
import "../../../src/layouts/hass-subpage";
|
import "../../../src/layouts/hass-subpage";
|
||||||
import { HomeAssistant, Route } from "../../../src/types";
|
import { HomeAssistant, Route } from "../../../src/types";
|
||||||
@ -45,6 +49,8 @@ class HassioIngressView extends LitElement {
|
|||||||
|
|
||||||
private _sessionKeepAlive?: number;
|
private _sessionKeepAlive?: number;
|
||||||
|
|
||||||
|
private _fetchDataTimeout?: number;
|
||||||
|
|
||||||
public disconnectedCallback() {
|
public disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
|
|
||||||
@ -52,16 +58,21 @@ class HassioIngressView extends LitElement {
|
|||||||
clearInterval(this._sessionKeepAlive);
|
clearInterval(this._sessionKeepAlive);
|
||||||
this._sessionKeepAlive = undefined;
|
this._sessionKeepAlive = undefined;
|
||||||
}
|
}
|
||||||
|
if (this._fetchDataTimeout) {
|
||||||
|
clearInterval(this._fetchDataTimeout);
|
||||||
|
this._fetchDataTimeout = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this._addon) {
|
if (!this._addon) {
|
||||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
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!}
|
||||||
|
@load=${this._checkLoaded}
|
||||||
>
|
>
|
||||||
</iframe>`;
|
</iframe>`;
|
||||||
|
|
||||||
@ -132,10 +143,10 @@ class HassioIngressView extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const addon = this.route.path.substr(1);
|
const addon = this.route.path.substring(1);
|
||||||
|
|
||||||
const oldRoute = changedProps.get("route") as this["route"] | undefined;
|
const oldRoute = changedProps.get("route") as this["route"] | undefined;
|
||||||
const oldAddon = oldRoute ? oldRoute.path.substr(1) : undefined;
|
const oldAddon = oldRoute ? oldRoute.path.substring(1) : undefined;
|
||||||
|
|
||||||
if (addon && addon !== oldAddon) {
|
if (addon && addon !== oldAddon) {
|
||||||
this._fetchData(addon);
|
this._fetchData(addon);
|
||||||
@ -145,33 +156,23 @@ class HassioIngressView extends LitElement {
|
|||||||
private async _fetchData(addonSlug: string) {
|
private async _fetchData(addonSlug: string) {
|
||||||
const createSessionPromise = createHassioSession(this.hass);
|
const createSessionPromise = createHassioSession(this.hass);
|
||||||
|
|
||||||
let addon;
|
let addon: HassioAddonDetails;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
|
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
await showAlertDialog(this, {
|
await showAlertDialog(this, {
|
||||||
text: "Unable to fetch add-on info to start Ingress",
|
text: this.supervisor.localize("ingress.error_addon_info"),
|
||||||
title: "Supervisor",
|
title: "Supervisor",
|
||||||
});
|
});
|
||||||
await nextRender();
|
await nextRender();
|
||||||
history.back();
|
navigate("/hassio/store", { replace: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!addon.ingress_url) {
|
if (!addon.version) {
|
||||||
await showAlertDialog(this, {
|
await showAlertDialog(this, {
|
||||||
text: "Add-on does not support Ingress",
|
text: this.supervisor.localize("ingress.error_addon_not_installed"),
|
||||||
title: addon.name,
|
|
||||||
});
|
|
||||||
await nextRender();
|
|
||||||
history.back();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addon.state !== "started") {
|
|
||||||
await showAlertDialog(this, {
|
|
||||||
text: "Add-on is not running. Please start it first",
|
|
||||||
title: addon.name,
|
title: addon.name,
|
||||||
});
|
});
|
||||||
await nextRender();
|
await nextRender();
|
||||||
@ -179,13 +180,74 @@ class HassioIngressView extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let session;
|
if (!addon.ingress_url) {
|
||||||
|
await showAlertDialog(this, {
|
||||||
|
text: this.supervisor.localize("ingress.error_addon_not_supported"),
|
||||||
|
title: addon.name,
|
||||||
|
});
|
||||||
|
await nextRender();
|
||||||
|
history.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!addon.state || !["startup", "started"].includes(addon.state)) {
|
||||||
|
const confirm = await showConfirmationDialog(this, {
|
||||||
|
text: this.supervisor.localize("ingress.error_addon_not_running"),
|
||||||
|
title: addon.name,
|
||||||
|
confirmText: this.supervisor.localize("ingress.start_addon"),
|
||||||
|
dismissText: this.supervisor.localize("common.no"),
|
||||||
|
});
|
||||||
|
if (confirm) {
|
||||||
|
try {
|
||||||
|
await startHassioAddon(this.hass, addonSlug);
|
||||||
|
fireEvent(this, "supervisor-collection-refresh", {
|
||||||
|
collection: "addon",
|
||||||
|
});
|
||||||
|
this._fetchData(addonSlug);
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
await showAlertDialog(this, {
|
||||||
|
text: this.supervisor.localize("ingress.error_starting_addon"),
|
||||||
|
title: addon.name,
|
||||||
|
});
|
||||||
|
await nextRender();
|
||||||
|
navigate(`/hassio/addon/${addon.slug}/logs`, { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await nextRender();
|
||||||
|
navigate(`/hassio/addon/${addon.slug}/info`, { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addon.state === "startup") {
|
||||||
|
// Addon is starting up, wait for it to start
|
||||||
|
this._fetchDataTimeout = window.setTimeout(() => {
|
||||||
|
this._fetchData(addonSlug);
|
||||||
|
}, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addon.state !== "started") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._fetchDataTimeout) {
|
||||||
|
clearInterval(this._fetchDataTimeout);
|
||||||
|
this._fetchDataTimeout = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let session: string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
session = await createSessionPromise;
|
session = await createSessionPromise;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
if (this._sessionKeepAlive) {
|
||||||
|
clearInterval(this._sessionKeepAlive);
|
||||||
|
}
|
||||||
await showAlertDialog(this, {
|
await showAlertDialog(this, {
|
||||||
text: "Unable to create an Ingress session",
|
text: this.supervisor.localize("ingress.error_creating_session"),
|
||||||
title: addon.name,
|
title: addon.name,
|
||||||
});
|
});
|
||||||
await nextRender();
|
await nextRender();
|
||||||
@ -207,6 +269,31 @@ class HassioIngressView extends LitElement {
|
|||||||
this._addon = addon;
|
this._addon = addon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _checkLoaded(ev): void {
|
||||||
|
if (!this._addon) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ev.target.contentDocument.body.textContent === "502: Bad Gateway") {
|
||||||
|
showConfirmationDialog(this, {
|
||||||
|
text: this.supervisor.localize("ingress.error_addon_not_ready"),
|
||||||
|
title: this._addon.name,
|
||||||
|
confirmText: this.supervisor.localize("ingress.retry"),
|
||||||
|
dismissText: this.supervisor.localize("common.no"),
|
||||||
|
confirm: async () => {
|
||||||
|
const addon = this._addon;
|
||||||
|
this._addon = undefined;
|
||||||
|
await Promise.all([
|
||||||
|
this.updateComplete,
|
||||||
|
new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 500);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
this._addon = addon;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _toggleMenu(): void {
|
private _toggleMenu(): void {
|
||||||
fireEvent(this, "hass-toggle-menu");
|
fireEvent(this, "hass-toggle-menu");
|
||||||
}
|
}
|
||||||
|
97
package.json
97
package.json
@ -25,33 +25,34 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.22.3",
|
"@babel/runtime": "7.22.5",
|
||||||
"@braintree/sanitize-url": "6.0.2",
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
"@codemirror/autocomplete": "6.7.1",
|
"@codemirror/autocomplete": "6.8.1",
|
||||||
"@codemirror/commands": "6.2.4",
|
"@codemirror/commands": "6.2.4",
|
||||||
"@codemirror/language": "6.7.0",
|
"@codemirror/language": "6.8.0",
|
||||||
"@codemirror/legacy-modes": "6.3.2",
|
"@codemirror/legacy-modes": "6.3.2",
|
||||||
"@codemirror/search": "6.4.0",
|
"@codemirror/search": "6.5.0",
|
||||||
"@codemirror/state": "6.2.1",
|
"@codemirror/state": "6.2.1",
|
||||||
"@codemirror/view": "6.12.0",
|
"@codemirror/view": "6.14.0",
|
||||||
"@egjs/hammerjs": "2.0.17",
|
"@egjs/hammerjs": "2.0.17",
|
||||||
"@formatjs/intl-datetimeformat": "6.8.0",
|
"@formatjs/intl-datetimeformat": "6.10.0",
|
||||||
"@formatjs/intl-displaynames": "6.3.2",
|
"@formatjs/intl-displaynames": "6.5.0",
|
||||||
"@formatjs/intl-getcanonicallocales": "2.2.0",
|
"@formatjs/intl-getcanonicallocales": "2.2.1",
|
||||||
"@formatjs/intl-locale": "3.3.0",
|
"@formatjs/intl-listformat": "7.4.0",
|
||||||
"@formatjs/intl-numberformat": "8.5.0",
|
"@formatjs/intl-locale": "3.3.2",
|
||||||
"@formatjs/intl-pluralrules": "5.2.2",
|
"@formatjs/intl-numberformat": "8.7.0",
|
||||||
"@formatjs/intl-relativetimeformat": "11.2.2",
|
"@formatjs/intl-pluralrules": "5.2.4",
|
||||||
|
"@formatjs/intl-relativetimeformat": "11.2.4",
|
||||||
"@fullcalendar/core": "6.1.8",
|
"@fullcalendar/core": "6.1.8",
|
||||||
"@fullcalendar/daygrid": "6.1.8",
|
"@fullcalendar/daygrid": "6.1.8",
|
||||||
"@fullcalendar/interaction": "6.1.8",
|
"@fullcalendar/interaction": "6.1.8",
|
||||||
"@fullcalendar/list": "6.1.8",
|
"@fullcalendar/list": "6.1.8",
|
||||||
"@fullcalendar/timegrid": "6.1.8",
|
"@fullcalendar/timegrid": "6.1.8",
|
||||||
"@lezer/highlight": "1.1.6",
|
"@lezer/highlight": "1.1.6",
|
||||||
"@lit-labs/context": "0.3.2",
|
"@lit-labs/context": "0.3.3",
|
||||||
"@lit-labs/motion": "1.0.3",
|
"@lit-labs/motion": "1.0.3",
|
||||||
"@lit-labs/virtualizer": "2.0.2",
|
"@lit-labs/virtualizer": "2.0.3",
|
||||||
"@lrnwebcomponents/simple-tooltip": "7.0.2",
|
"@lrnwebcomponents/simple-tooltip": "7.0.5",
|
||||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/mwc-button": "0.27.0",
|
"@material/mwc-button": "0.27.0",
|
||||||
@ -77,7 +78,7 @@
|
|||||||
"@material/mwc-top-app-bar": "0.27.0",
|
"@material/mwc-top-app-bar": "0.27.0",
|
||||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/web": "=1.0.0-pre.9",
|
"@material/web": "=1.0.0-pre.11",
|
||||||
"@mdi/js": "7.2.96",
|
"@mdi/js": "7.2.96",
|
||||||
"@mdi/svg": "7.2.96",
|
"@mdi/svg": "7.2.96",
|
||||||
"@polymer/app-layout": "3.1.0",
|
"@polymer/app-layout": "3.1.0",
|
||||||
@ -92,8 +93,8 @@
|
|||||||
"@polymer/paper-toast": "3.0.1",
|
"@polymer/paper-toast": "3.0.1",
|
||||||
"@polymer/polymer": "3.5.1",
|
"@polymer/polymer": "3.5.1",
|
||||||
"@thomasloven/round-slider": "0.6.0",
|
"@thomasloven/round-slider": "0.6.0",
|
||||||
"@vaadin/combo-box": "24.0.8",
|
"@vaadin/combo-box": "24.1.1",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.0.8",
|
"@vaadin/vaadin-themable-mixin": "24.1.1",
|
||||||
"@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",
|
||||||
@ -103,7 +104,7 @@
|
|||||||
"app-datepicker": "5.1.1",
|
"app-datepicker": "5.1.1",
|
||||||
"chart.js": "3.3.2",
|
"chart.js": "3.3.2",
|
||||||
"comlink": "4.4.1",
|
"comlink": "4.4.1",
|
||||||
"core-js": "3.30.2",
|
"core-js": "3.31.0",
|
||||||
"cropperjs": "1.5.13",
|
"cropperjs": "1.5.13",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"date-fns-tz": "2.0.0",
|
"date-fns-tz": "2.0.0",
|
||||||
@ -111,10 +112,10 @@
|
|||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"fuse.js": "6.6.2",
|
"fuse.js": "6.6.2",
|
||||||
"google-timezones-json": "1.1.0",
|
"google-timezones-json": "1.1.0",
|
||||||
"hls.js": "1.4.4",
|
"hls.js": "1.4.6",
|
||||||
"home-assistant-js-websocket": "8.0.1",
|
"home-assistant-js-websocket": "8.1.0",
|
||||||
"idb-keyval": "6.2.1",
|
"idb-keyval": "6.2.1",
|
||||||
"intl-messageformat": "10.3.5",
|
"intl-messageformat": "10.5.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-draw": "1.0.4",
|
"leaflet-draw": "1.0.4",
|
||||||
@ -131,9 +132,9 @@
|
|||||||
"rrule": "2.7.2",
|
"rrule": "2.7.2",
|
||||||
"sortablejs": "1.15.0",
|
"sortablejs": "1.15.0",
|
||||||
"superstruct": "1.0.3",
|
"superstruct": "1.0.3",
|
||||||
"tinykeys": "1.4.0",
|
"tinykeys": "2.1.0",
|
||||||
"tsparticles-engine": "2.9.3",
|
"tsparticles-engine": "2.10.1",
|
||||||
"tsparticles-preset-links": "2.9.3",
|
"tsparticles-preset-links": "2.10.1",
|
||||||
"unfetch": "5.0.0",
|
"unfetch": "5.0.0",
|
||||||
"vis-data": "7.1.6",
|
"vis-data": "7.1.6",
|
||||||
"vis-network": "9.1.6",
|
"vis-network": "9.1.6",
|
||||||
@ -149,18 +150,18 @@
|
|||||||
"xss": "1.0.14"
|
"xss": "1.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.22.1",
|
"@babel/core": "7.22.5",
|
||||||
"@babel/plugin-proposal-decorators": "7.22.3",
|
"@babel/plugin-proposal-decorators": "7.22.5",
|
||||||
"@babel/plugin-transform-runtime": "7.22.4",
|
"@babel/plugin-transform-runtime": "7.22.5",
|
||||||
"@babel/preset-env": "7.22.4",
|
"@babel/preset-env": "7.22.5",
|
||||||
"@babel/preset-typescript": "7.21.5",
|
"@babel/preset-typescript": "7.22.5",
|
||||||
"@koa/cors": "4.0.0",
|
"@koa/cors": "4.0.0",
|
||||||
"@octokit/auth-oauth-device": "4.0.4",
|
"@octokit/auth-oauth-device": "5.0.2",
|
||||||
"@octokit/plugin-retry": "5.0.0",
|
"@octokit/plugin-retry": "5.0.4",
|
||||||
"@octokit/rest": "19.0.11",
|
"@octokit/rest": "19.0.13",
|
||||||
"@open-wc/dev-server-hmr": "0.1.4",
|
"@open-wc/dev-server-hmr": "0.1.4",
|
||||||
"@rollup/plugin-babel": "6.0.3",
|
"@rollup/plugin-babel": "6.0.3",
|
||||||
"@rollup/plugin-commonjs": "25.0.0",
|
"@rollup/plugin-commonjs": "25.0.2",
|
||||||
"@rollup/plugin-json": "6.0.0",
|
"@rollup/plugin-json": "6.0.0",
|
||||||
"@rollup/plugin-node-resolve": "15.1.0",
|
"@rollup/plugin-node-resolve": "15.1.0",
|
||||||
"@rollup/plugin-replace": "5.0.2",
|
"@rollup/plugin-replace": "5.0.2",
|
||||||
@ -172,7 +173,7 @@
|
|||||||
"@types/html-minifier-terser": "7.0.0",
|
"@types/html-minifier-terser": "7.0.0",
|
||||||
"@types/js-yaml": "4.0.5",
|
"@types/js-yaml": "4.0.5",
|
||||||
"@types/leaflet": "1.9.3",
|
"@types/leaflet": "1.9.3",
|
||||||
"@types/leaflet-draw": "1.0.6",
|
"@types/leaflet-draw": "1.0.7",
|
||||||
"@types/marked": "4.3.1",
|
"@types/marked": "4.3.1",
|
||||||
"@types/mocha": "10.0.1",
|
"@types/mocha": "10.0.1",
|
||||||
"@types/qrcode": "1.5.0",
|
"@types/qrcode": "1.5.0",
|
||||||
@ -180,15 +181,15 @@
|
|||||||
"@types/sortablejs": "1.15.1",
|
"@types/sortablejs": "1.15.1",
|
||||||
"@types/tar": "6.1.5",
|
"@types/tar": "6.1.5",
|
||||||
"@types/webspeechapi": "0.0.29",
|
"@types/webspeechapi": "0.0.29",
|
||||||
"@typescript-eslint/eslint-plugin": "5.59.8",
|
"@typescript-eslint/eslint-plugin": "5.60.0",
|
||||||
"@typescript-eslint/parser": "5.59.8",
|
"@typescript-eslint/parser": "5.60.0",
|
||||||
"@web/dev-server": "0.1.38",
|
"@web/dev-server": "0.1.38",
|
||||||
"@web/dev-server-rollup": "0.4.1",
|
"@web/dev-server-rollup": "0.4.1",
|
||||||
"babel-loader": "9.1.2",
|
"babel-loader": "9.1.2",
|
||||||
"babel-plugin-template-html-minifier": "4.1.0",
|
"babel-plugin-template-html-minifier": "4.1.0",
|
||||||
"chai": "4.3.7",
|
"chai": "4.3.7",
|
||||||
"del": "7.0.0",
|
"del": "7.0.0",
|
||||||
"eslint": "8.42.0",
|
"eslint": "8.43.0",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-airbnb-typescript": "17.0.0",
|
"eslint-config-airbnb-typescript": "17.0.0",
|
||||||
"eslint-config-prettier": "8.8.0",
|
"eslint-config-prettier": "8.8.0",
|
||||||
@ -196,13 +197,13 @@
|
|||||||
"eslint-plugin-disable": "2.0.3",
|
"eslint-plugin-disable": "2.0.3",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"eslint-plugin-lit": "1.8.3",
|
"eslint-plugin-lit": "1.8.3",
|
||||||
"eslint-plugin-lit-a11y": "2.4.1",
|
"eslint-plugin-lit-a11y": "3.0.0",
|
||||||
"eslint-plugin-unused-imports": "2.0.0",
|
"eslint-plugin-unused-imports": "2.0.0",
|
||||||
"eslint-plugin-wc": "1.5.0",
|
"eslint-plugin-wc": "1.5.0",
|
||||||
"esprima": "4.0.1",
|
"esprima": "4.0.1",
|
||||||
"fancy-log": "2.0.0",
|
"fancy-log": "2.0.0",
|
||||||
"fs-extra": "11.1.1",
|
"fs-extra": "11.1.1",
|
||||||
"glob": "10.2.6",
|
"glob": "10.3.0",
|
||||||
"gulp": "4.0.2",
|
"gulp": "4.0.2",
|
||||||
"gulp-flatmap": "1.0.2",
|
"gulp-flatmap": "1.0.2",
|
||||||
"gulp-json-transform": "0.4.8",
|
"gulp-json-transform": "0.4.8",
|
||||||
@ -214,7 +215,7 @@
|
|||||||
"instant-mocha": "1.5.1",
|
"instant-mocha": "1.5.1",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lint-staged": "13.2.2",
|
"lint-staged": "13.2.2",
|
||||||
"lit-analyzer": "1.2.1",
|
"lit-analyzer": "2.0.0-pre.3",
|
||||||
"lodash.template": "4.5.0",
|
"lodash.template": "4.5.0",
|
||||||
"magic-string": "0.30.0",
|
"magic-string": "0.30.0",
|
||||||
"map-stream": "0.0.7",
|
"map-stream": "0.0.7",
|
||||||
@ -227,20 +228,20 @@
|
|||||||
"rollup": "2.79.1",
|
"rollup": "2.79.1",
|
||||||
"rollup-plugin-string": "3.0.0",
|
"rollup-plugin-string": "3.0.0",
|
||||||
"rollup-plugin-terser": "7.0.2",
|
"rollup-plugin-terser": "7.0.2",
|
||||||
"rollup-plugin-visualizer": "5.9.0",
|
"rollup-plugin-visualizer": "5.9.2",
|
||||||
"serve-handler": "6.1.5",
|
"serve-handler": "6.1.5",
|
||||||
"sinon": "15.1.0",
|
"sinon": "15.2.0",
|
||||||
"source-map-url": "0.4.1",
|
"source-map-url": "0.4.1",
|
||||||
"systemjs": "6.14.1",
|
"systemjs": "6.14.1",
|
||||||
"tar": "6.1.15",
|
"tar": "6.1.15",
|
||||||
"terser-webpack-plugin": "5.3.9",
|
"terser-webpack-plugin": "5.3.9",
|
||||||
"ts-lit-plugin": "1.2.1",
|
"ts-lit-plugin": "2.0.0-pre.1",
|
||||||
"typescript": "4.9.5",
|
"typescript": "5.1.3",
|
||||||
"vinyl-buffer": "1.0.1",
|
"vinyl-buffer": "1.0.1",
|
||||||
"vinyl-source-stream": "2.0.0",
|
"vinyl-source-stream": "2.0.0",
|
||||||
"webpack": "5.84.1",
|
"webpack": "5.88.0",
|
||||||
"webpack-cli": "5.1.1",
|
"webpack-cli": "5.1.4",
|
||||||
"webpack-dev-server": "4.15.0",
|
"webpack-dev-server": "4.15.1",
|
||||||
"webpack-manifest-plugin": "5.0.0",
|
"webpack-manifest-plugin": "5.0.0",
|
||||||
"webpackbar": "5.0.2",
|
"webpackbar": "5.0.2",
|
||||||
"workbox-build": "7.0.0"
|
"workbox-build": "7.0.0"
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools~=62.3", "wheel~=0.37.1"]
|
requires = ["setuptools~=68.0", "wheel~=0.40.0"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20230608.0"
|
version = "20230628.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"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
|
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
|
||||||
]
|
]
|
||||||
requires-python = ">=3.4.0"
|
requires-python = ">=3.10.0"
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
"Homepage" = "https://github.com/home-assistant/frontend"
|
"Homepage" = "https://github.com/home-assistant/frontend"
|
||||||
|
@ -19,14 +19,20 @@
|
|||||||
},
|
},
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"description": ["MDC packages are pinned to the same version as MWC"],
|
"description": "MDC packages are pinned to the same version as MWC",
|
||||||
"extends": ["monorepo:material-components-web"],
|
"extends": ["monorepo:material-components-web"],
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": ["Vue is only used by date range which is only v2"],
|
"description": "Vue is only used by date range which is only v2",
|
||||||
"matchPackageNames": ["vue"],
|
"matchPackageNames": ["vue"],
|
||||||
"allowedVersions": "< 3"
|
"allowedVersions": "< 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Group tsparticles engine and presets",
|
||||||
|
"groupName": "tsparticles",
|
||||||
|
"matchPackageNames": ["tsparticles-engine"],
|
||||||
|
"matchPackagePrefixes": ["tsparticles-preset-"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,9 @@ cd "$(dirname "$0")/.."
|
|||||||
|
|
||||||
# Install/upgrade node when inside devcontainer
|
# Install/upgrade node when inside devcontainer
|
||||||
if [[ -n "$DEVCONTAINER" ]]; then
|
if [[ -n "$DEVCONTAINER" ]]; then
|
||||||
nodeCurrent=$(nvm version default || echo "")
|
nodeCurrent=$(nvm version default || :)
|
||||||
nodeLatest=$(nvm version-remote "$(cat .nvmrc)")
|
nodeLatest=$(nvm version-remote "$(cat .nvmrc)")
|
||||||
if [[ -z "$nodeCurrent" ]]; then
|
if [[ -z "$nodeCurrent" || "$nodeCurrent" == "N/A" ]]; then
|
||||||
nvm install
|
nvm install
|
||||||
elif [[ "$nodeCurrent" != "$nodeLatest" ]]; then
|
elif [[ "$nodeCurrent" != "$nodeLatest" ]]; then
|
||||||
nvm install --reinstall-packages-from="$nodeCurrent" --default
|
nvm install --reinstall-packages-from="$nodeCurrent" --default
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
# Setuptools v62.3 doesn't support editable installs with just 'pyproject.toml' (PEP 660).
|
|
||||||
# Keep this file until it does!
|
|
@ -33,6 +33,7 @@ import {
|
|||||||
mdiGoogleCirclesCommunities,
|
mdiGoogleCirclesCommunities,
|
||||||
mdiHomeAssistant,
|
mdiHomeAssistant,
|
||||||
mdiHomeAutomation,
|
mdiHomeAutomation,
|
||||||
|
mdiImage,
|
||||||
mdiImageFilterFrames,
|
mdiImageFilterFrames,
|
||||||
mdiLightbulb,
|
mdiLightbulb,
|
||||||
mdiLightningBolt,
|
mdiLightningBolt,
|
||||||
@ -90,6 +91,7 @@ export const FIXED_DOMAIN_ICONS = {
|
|||||||
group: mdiGoogleCirclesCommunities,
|
group: mdiGoogleCirclesCommunities,
|
||||||
homeassistant: mdiHomeAssistant,
|
homeassistant: mdiHomeAssistant,
|
||||||
homekit: mdiHomeAutomation,
|
homekit: mdiHomeAutomation,
|
||||||
|
image: mdiImage,
|
||||||
image_processing: mdiImageFilterFrames,
|
image_processing: mdiImageFilterFrames,
|
||||||
input_button: mdiGestureTapButton,
|
input_button: mdiGestureTapButton,
|
||||||
input_datetime: mdiCalendarClock,
|
input_datetime: mdiCalendarClock,
|
||||||
@ -180,6 +182,7 @@ export const DOMAINS_WITH_CARD = [
|
|||||||
"input_select",
|
"input_select",
|
||||||
"input_number",
|
"input_number",
|
||||||
"input_text",
|
"input_text",
|
||||||
|
"humidifier",
|
||||||
"lock",
|
"lock",
|
||||||
"media_player",
|
"media_player",
|
||||||
"number",
|
"number",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { isSameDay, isSameYear } from "date-fns";
|
import { isSameDay, isSameYear } from "date-fns";
|
||||||
|
import { HassConfig } from "home-assistant-js-websocket";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
import { FrontendLocaleData } from "../../data/translation";
|
||||||
import {
|
import {
|
||||||
formatShortDateTime,
|
formatShortDateTime,
|
||||||
@ -9,15 +10,16 @@ import { formatTime } from "./format_time";
|
|||||||
export const absoluteTime = (
|
export const absoluteTime = (
|
||||||
from: Date,
|
from: Date,
|
||||||
locale: FrontendLocaleData,
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig,
|
||||||
to?: Date
|
to?: Date
|
||||||
): string => {
|
): string => {
|
||||||
const _to = to ?? new Date();
|
const _to = to ?? new Date();
|
||||||
|
|
||||||
if (isSameDay(from, _to)) {
|
if (isSameDay(from, _to)) {
|
||||||
return formatTime(from, locale);
|
return formatTime(from, locale, config);
|
||||||
}
|
}
|
||||||
if (isSameYear(from, _to)) {
|
if (isSameYear(from, _to)) {
|
||||||
return formatShortDateTime(from, locale);
|
return formatShortDateTime(from, locale, config);
|
||||||
}
|
}
|
||||||
return formatShortDateTimeWithYear(from, locale);
|
return formatShortDateTimeWithYear(from, locale, config);
|
||||||
};
|
};
|
||||||
|
25
src/common/datetime/calc_date.ts
Normal file
25
src/common/datetime/calc_date.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz";
|
||||||
|
import { HassConfig } from "home-assistant-js-websocket";
|
||||||
|
import { FrontendLocaleData, TimeZone } from "../../data/translation";
|
||||||
|
|
||||||
|
const calcZonedDate = (
|
||||||
|
date: Date,
|
||||||
|
tz: string,
|
||||||
|
fn: (date: Date, options?: any) => Date,
|
||||||
|
options?
|
||||||
|
) => {
|
||||||
|
const inputZoned = utcToZonedTime(date, tz);
|
||||||
|
const fnZoned = fn(inputZoned, options);
|
||||||
|
return zonedTimeToUtc(fnZoned, tz);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const calcDate = (
|
||||||
|
date: Date,
|
||||||
|
fn: (date: Date, options?: any) => Date,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig,
|
||||||
|
options?
|
||||||
|
) =>
|
||||||
|
locale.time_zone === TimeZone.server
|
||||||
|
? calcZonedDate(date, config.time_zone, fn, options)
|
||||||
|
: fn(date, options);
|
@ -1,3 +1,4 @@
|
|||||||
|
import { HassConfig } from "home-assistant-js-websocket";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { FrontendLocaleData, DateFormat } from "../../data/translation";
|
import { FrontendLocaleData, DateFormat } from "../../data/translation";
|
||||||
import "../../resources/intl-polyfill";
|
import "../../resources/intl-polyfill";
|
||||||
@ -5,37 +6,44 @@ import "../../resources/intl-polyfill";
|
|||||||
// Tuesday, August 10
|
// Tuesday, August 10
|
||||||
export const formatDateWeekdayDay = (
|
export const formatDateWeekdayDay = (
|
||||||
dateObj: Date,
|
dateObj: Date,
|
||||||
locale: FrontendLocaleData
|
locale: FrontendLocaleData,
|
||||||
) => formatDateWeekdayDayMem(locale).format(dateObj);
|
config: HassConfig
|
||||||
|
) => formatDateWeekdayDayMem(locale, config.time_zone).format(dateObj);
|
||||||
|
|
||||||
const formatDateWeekdayDayMem = memoizeOne(
|
const formatDateWeekdayDayMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
weekday: "long",
|
weekday: "long",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// August 10, 2021
|
// August 10, 2021
|
||||||
export const formatDate = (dateObj: Date, locale: FrontendLocaleData) =>
|
export const formatDate = (
|
||||||
formatDateMem(locale).format(dateObj);
|
dateObj: Date,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig
|
||||||
|
) => formatDateMem(locale, config.time_zone).format(dateObj);
|
||||||
|
|
||||||
const formatDateMem = memoizeOne(
|
const formatDateMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 10/08/2021
|
// 10/08/2021
|
||||||
export const formatDateNumeric = (
|
export const formatDateNumeric = (
|
||||||
dateObj: Date,
|
dateObj: Date,
|
||||||
locale: FrontendLocaleData
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig
|
||||||
) => {
|
) => {
|
||||||
const formatter = formatDateNumericMem(locale);
|
const formatter = formatDateNumericMem(locale, config.time_zone);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
locale.date_format === DateFormat.language ||
|
locale.date_format === DateFormat.language ||
|
||||||
@ -67,7 +75,8 @@ export const formatDateNumeric = (
|
|||||||
return formats[locale.date_format];
|
return formats[locale.date_format];
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDateNumericMem = memoizeOne((locale: FrontendLocaleData) => {
|
const formatDateNumericMem = memoizeOne(
|
||||||
|
(locale: FrontendLocaleData, serverTimeZone: string) => {
|
||||||
const localeString =
|
const localeString =
|
||||||
locale.date_format === DateFormat.system ? undefined : locale.language;
|
locale.date_format === DateFormat.system ? undefined : locale.language;
|
||||||
|
|
||||||
@ -79,6 +88,7 @@ const formatDateNumericMem = memoizeOne((locale: FrontendLocaleData) => {
|
|||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "numeric",
|
month: "numeric",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,64 +96,99 @@ const formatDateNumericMem = memoizeOne((locale: FrontendLocaleData) => {
|
|||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "numeric",
|
month: "numeric",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Aug 10
|
// Aug 10
|
||||||
export const formatDateShort = (dateObj: Date, locale: FrontendLocaleData) =>
|
export const formatDateShort = (
|
||||||
formatDateShortMem(locale).format(dateObj);
|
dateObj: Date,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig
|
||||||
|
) => formatDateShortMem(locale, config.time_zone).format(dateObj);
|
||||||
|
|
||||||
const formatDateShortMem = memoizeOne(
|
const formatDateShortMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// August 2021
|
// August 2021
|
||||||
export const formatDateMonthYear = (
|
export const formatDateMonthYear = (
|
||||||
dateObj: Date,
|
dateObj: Date,
|
||||||
locale: FrontendLocaleData
|
locale: FrontendLocaleData,
|
||||||
) => formatDateMonthYearMem(locale).format(dateObj);
|
config: HassConfig
|
||||||
|
) => formatDateMonthYearMem(locale, config.time_zone).format(dateObj);
|
||||||
|
|
||||||
const formatDateMonthYearMem = memoizeOne(
|
const formatDateMonthYearMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// August
|
// August
|
||||||
export const formatDateMonth = (dateObj: Date, locale: FrontendLocaleData) =>
|
export const formatDateMonth = (
|
||||||
formatDateMonthMem(locale).format(dateObj);
|
dateObj: Date,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig
|
||||||
|
) => formatDateMonthMem(locale, config.time_zone).format(dateObj);
|
||||||
|
|
||||||
const formatDateMonthMem = memoizeOne(
|
const formatDateMonthMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
month: "long",
|
month: "long",
|
||||||
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2021
|
// 2021
|
||||||
export const formatDateYear = (dateObj: Date, locale: FrontendLocaleData) =>
|
export const formatDateYear = (
|
||||||
formatDateYearMem(locale).format(dateObj);
|
dateObj: Date,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig
|
||||||
|
) => formatDateYearMem(locale, config.time_zone).format(dateObj);
|
||||||
|
|
||||||
const formatDateYearMem = memoizeOne(
|
const formatDateYearMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Monday
|
// Monday
|
||||||
export const formatDateWeekday = (dateObj: Date, locale: FrontendLocaleData) =>
|
export const formatDateWeekday = (
|
||||||
formatDateWeekdayMem(locale).format(dateObj);
|
dateObj: Date,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig
|
||||||
|
) => formatDateWeekdayMem(locale, config.time_zone).format(dateObj);
|
||||||
|
|
||||||
const formatDateWeekdayMem = memoizeOne(
|
const formatDateWeekdayMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||||
new Intl.DateTimeFormat(locale.language, {
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
weekday: "long",
|
weekday: "long",
|
||||||
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mon
|
||||||
|
export const formatDateWeekdayShort = (
|
||||||
|
dateObj: Date,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig
|
||||||
|
) => formatDateWeekdayShortMem(locale, config.time_zone).format(dateObj);
|
||||||
|
|
||||||
|
const formatDateWeekdayShortMem = memoizeOne(
|
||||||
|
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||||
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
|
weekday: "short",
|
||||||
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -1,102 +1,99 @@
|
|||||||
|
import { HassConfig } from "home-assistant-js-websocket";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
import { FrontendLocaleData } from "../../data/translation";
|
||||||
import "../../resources/intl-polyfill";
|
import "../../resources/intl-polyfill";
|
||||||
import { useAmPm } from "./use_am_pm";
|
|
||||||
import { formatDateNumeric } from "./format_date";
|
import { formatDateNumeric } from "./format_date";
|
||||||
import { formatTime } from "./format_time";
|
import { formatTime } from "./format_time";
|
||||||
|
import { useAmPm } from "./use_am_pm";
|
||||||
|
|
||||||
// August 9, 2021, 8:23 AM
|
// August 9, 2021, 8:23 AM
|
||||||
export const formatDateTime = (dateObj: Date, locale: FrontendLocaleData) =>
|
export const formatDateTime = (
|
||||||
formatDateTimeMem(locale).format(dateObj);
|
dateObj: Date,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig
|
||||||
|
) => formatDateTimeMem(locale, config.time_zone).format(dateObj);
|
||||||
|
|
||||||
const formatDateTimeMem = memoizeOne(
|
const formatDateTimeMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||||
new Intl.DateTimeFormat(
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
locale.language === "en" && !useAmPm(locale)
|
|
||||||
? "en-u-hc-h23"
|
|
||||||
: locale.language,
|
|
||||||
{
|
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
hour12: useAmPm(locale),
|
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||||
}
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
)
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Aug 9, 2021, 8:23 AM
|
// Aug 9, 2021, 8:23 AM
|
||||||
export const formatShortDateTimeWithYear = (
|
export const formatShortDateTimeWithYear = (
|
||||||
dateObj: Date,
|
dateObj: Date,
|
||||||
locale: FrontendLocaleData
|
locale: FrontendLocaleData,
|
||||||
) => formatShortDateTimeWithYearMem(locale).format(dateObj);
|
config: HassConfig
|
||||||
|
) => formatShortDateTimeWithYearMem(locale, config.time_zone).format(dateObj);
|
||||||
|
|
||||||
const formatShortDateTimeWithYearMem = memoizeOne(
|
const formatShortDateTimeWithYearMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||||
new Intl.DateTimeFormat(
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
locale.language === "en" && !useAmPm(locale)
|
|
||||||
? "en-u-hc-h23"
|
|
||||||
: locale.language,
|
|
||||||
{
|
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
hour12: useAmPm(locale),
|
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||||
}
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
)
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Aug 9, 8:23 AM
|
// Aug 9, 8:23 AM
|
||||||
export const formatShortDateTime = (
|
export const formatShortDateTime = (
|
||||||
dateObj: Date,
|
dateObj: Date,
|
||||||
locale: FrontendLocaleData
|
locale: FrontendLocaleData,
|
||||||
) => formatShortDateTimeMem(locale).format(dateObj);
|
config: HassConfig
|
||||||
|
) => formatShortDateTimeMem(locale, config.time_zone).format(dateObj);
|
||||||
|
|
||||||
const formatShortDateTimeMem = memoizeOne(
|
const formatShortDateTimeMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||||
new Intl.DateTimeFormat(
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
locale.language === "en" && !useAmPm(locale)
|
|
||||||
? "en-u-hc-h23"
|
|
||||||
: locale.language,
|
|
||||||
{
|
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
hour12: useAmPm(locale),
|
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||||
}
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
)
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// August 9, 2021, 8:23:15 AM
|
// August 9, 2021, 8:23:15 AM
|
||||||
export const formatDateTimeWithSeconds = (
|
export const formatDateTimeWithSeconds = (
|
||||||
dateObj: Date,
|
dateObj: Date,
|
||||||
locale: FrontendLocaleData
|
locale: FrontendLocaleData,
|
||||||
) => formatDateTimeWithSecondsMem(locale).format(dateObj);
|
config: HassConfig
|
||||||
|
) => formatDateTimeWithSecondsMem(locale, config.time_zone).format(dateObj);
|
||||||
|
|
||||||
const formatDateTimeWithSecondsMem = memoizeOne(
|
const formatDateTimeWithSecondsMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||||
new Intl.DateTimeFormat(
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
locale.language === "en" && !useAmPm(locale)
|
|
||||||
? "en-u-hc-h23"
|
|
||||||
: locale.language,
|
|
||||||
{
|
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
hour12: useAmPm(locale),
|
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||||
}
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
)
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 9/8/2021, 8:23 AM
|
// 9/8/2021, 8:23 AM
|
||||||
export const formatDateTimeNumeric = (
|
export const formatDateTimeNumeric = (
|
||||||
dateObj: Date,
|
dateObj: Date,
|
||||||
locale: FrontendLocaleData
|
locale: FrontendLocaleData,
|
||||||
) => `${formatDateNumeric(dateObj, locale)}, ${formatTime(dateObj, locale)}`;
|
config: HassConfig
|
||||||
|
) =>
|
||||||
|
`${formatDateNumeric(dateObj, locale, config)}, ${formatTime(
|
||||||
|
dateObj,
|
||||||
|
locale,
|
||||||
|
config
|
||||||
|
)}`;
|
||||||
|
@ -1,76 +1,76 @@
|
|||||||
|
import { HassConfig } from "home-assistant-js-websocket";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
import { FrontendLocaleData } from "../../data/translation";
|
||||||
import "../../resources/intl-polyfill";
|
import "../../resources/intl-polyfill";
|
||||||
import { useAmPm } from "./use_am_pm";
|
import { useAmPm } from "./use_am_pm";
|
||||||
|
|
||||||
// 9:15 PM || 21:15
|
// 9:15 PM || 21:15
|
||||||
export const formatTime = (dateObj: Date, locale: FrontendLocaleData) =>
|
export const formatTime = (
|
||||||
formatTimeMem(locale).format(dateObj);
|
dateObj: Date,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig
|
||||||
|
) => formatTimeMem(locale, config.time_zone).format(dateObj);
|
||||||
|
|
||||||
const formatTimeMem = memoizeOne(
|
const formatTimeMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||||
new Intl.DateTimeFormat(
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
locale.language === "en" && !useAmPm(locale)
|
|
||||||
? "en-u-hc-h23"
|
|
||||||
: locale.language,
|
|
||||||
{
|
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
hour12: useAmPm(locale),
|
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||||
}
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
)
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 9:15:24 PM || 21:15:24
|
// 9:15:24 PM || 21:15:24
|
||||||
export const formatTimeWithSeconds = (
|
export const formatTimeWithSeconds = (
|
||||||
dateObj: Date,
|
dateObj: Date,
|
||||||
locale: FrontendLocaleData
|
locale: FrontendLocaleData,
|
||||||
) => formatTimeWithSecondsMem(locale).format(dateObj);
|
config: HassConfig
|
||||||
|
) => formatTimeWithSecondsMem(locale, config.time_zone).format(dateObj);
|
||||||
|
|
||||||
const formatTimeWithSecondsMem = memoizeOne(
|
const formatTimeWithSecondsMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||||
new Intl.DateTimeFormat(
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
locale.language === "en" && !useAmPm(locale)
|
|
||||||
? "en-u-hc-h23"
|
|
||||||
: locale.language,
|
|
||||||
{
|
|
||||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
hour12: useAmPm(locale),
|
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||||
}
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
)
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Tuesday 7:00 PM || Tuesday 19:00
|
// Tuesday 7:00 PM || Tuesday 19:00
|
||||||
export const formatTimeWeekday = (dateObj: Date, locale: FrontendLocaleData) =>
|
export const formatTimeWeekday = (
|
||||||
formatTimeWeekdayMem(locale).format(dateObj);
|
dateObj: Date,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig
|
||||||
|
) => formatTimeWeekdayMem(locale, config.time_zone).format(dateObj);
|
||||||
|
|
||||||
const formatTimeWeekdayMem = memoizeOne(
|
const formatTimeWeekdayMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||||
new Intl.DateTimeFormat(
|
new Intl.DateTimeFormat(locale.language, {
|
||||||
locale.language === "en" && !useAmPm(locale)
|
|
||||||
? "en-u-hc-h23"
|
|
||||||
: locale.language,
|
|
||||||
{
|
|
||||||
weekday: "long",
|
weekday: "long",
|
||||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
hour12: useAmPm(locale),
|
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||||
}
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
)
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 21:15
|
// 21:15
|
||||||
export const formatTime24h = (dateObj: Date) =>
|
export const formatTime24h = (
|
||||||
formatTime24hMem().format(dateObj);
|
dateObj: Date,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig
|
||||||
|
) => formatTime24hMem(locale, config.time_zone).format(dateObj);
|
||||||
|
|
||||||
const formatTime24hMem = memoizeOne(
|
const formatTime24hMem = memoizeOne(
|
||||||
() =>
|
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||||
// en-GB to fix Chrome 24:59 to 0:59 https://stackoverflow.com/a/60898146
|
// en-GB to fix Chrome 24:59 to 0:59 https://stackoverflow.com/a/60898146
|
||||||
new Intl.DateTimeFormat("en-GB", {
|
new Intl.DateTimeFormat("en-GB", {
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
hour12: false,
|
hour12: false,
|
||||||
|
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -8,8 +8,10 @@ export const useAmPm = memoizeOne((locale: FrontendLocaleData): boolean => {
|
|||||||
) {
|
) {
|
||||||
const testLanguage =
|
const testLanguage =
|
||||||
locale.time_format === TimeFormat.language ? locale.language : undefined;
|
locale.time_format === TimeFormat.language ? locale.language : undefined;
|
||||||
const test = new Date().toLocaleString(testLanguage);
|
const test = new Date("January 1, 2023 22:00:00").toLocaleString(
|
||||||
return test.includes("AM") || test.includes("PM");
|
testLanguage
|
||||||
|
);
|
||||||
|
return test.includes("10");
|
||||||
}
|
}
|
||||||
|
|
||||||
return locale.time_format === TimeFormat.am_pm;
|
return locale.time_format === TimeFormat.am_pm;
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { PropertyDeclaration, ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
|
import { InternalPropertyDeclaration } from "lit/decorators";
|
||||||
import type { ClassElement } from "../../types";
|
import type { ClassElement } from "../../types";
|
||||||
|
|
||||||
type Callback = (oldValue: any, newValue: any) => void;
|
type Callback = (oldValue: any, newValue: any) => void;
|
||||||
|
|
||||||
class Storage {
|
class StorageClass {
|
||||||
constructor(subscribe = true, storage = window.localStorage) {
|
constructor(storage = window.localStorage) {
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
if (!subscribe) {
|
if (storage !== window.localStorage) {
|
||||||
|
// storage events only work for localStorage
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.addEventListener("storage", (ev: StorageEvent) => {
|
window.addEventListener("storage", (ev: StorageEvent) => {
|
||||||
@ -77,6 +79,7 @@ class Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setValue(storageKey: string, value: any): any {
|
public setValue(storageKey: string, value: any): any {
|
||||||
|
const oldValue = this._storage[storageKey];
|
||||||
this._storage[storageKey] = value;
|
this._storage[storageKey] = value;
|
||||||
try {
|
try {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
@ -86,49 +89,68 @@ class Storage {
|
|||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// Safari in private mode doesn't allow localstorage
|
// Safari in private mode doesn't allow localstorage
|
||||||
|
} finally {
|
||||||
|
if (this._listeners[storageKey]) {
|
||||||
|
this._listeners[storageKey].forEach((listener) =>
|
||||||
|
listener(oldValue, value)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscribeStorage = new Storage();
|
const storages: Record<string, StorageClass> = {};
|
||||||
|
|
||||||
export const LocalStorage =
|
export const storage =
|
||||||
(
|
(options: {
|
||||||
storageKey?: string,
|
key?: string;
|
||||||
property?: boolean,
|
storage?: "localStorage" | "sessionStorage";
|
||||||
subscribe = true,
|
subscribe?: boolean;
|
||||||
storageType?: globalThis.Storage,
|
state?: boolean;
|
||||||
propertyOptions?: PropertyDeclaration
|
stateOptions?: InternalPropertyDeclaration;
|
||||||
): any =>
|
}): any =>
|
||||||
(clsElement: ClassElement) => {
|
(clsElement: ClassElement) => {
|
||||||
const storage =
|
const storageName = options.storage || "localStorage";
|
||||||
subscribe && !storageType
|
|
||||||
? subscribeStorage
|
let storageInstance: StorageClass;
|
||||||
: new Storage(subscribe, storageType);
|
if (storageName && storageName in storages) {
|
||||||
|
storageInstance = storages[storageName];
|
||||||
|
} else {
|
||||||
|
storageInstance = new StorageClass(window[storageName]);
|
||||||
|
storages[storageName] = storageInstance;
|
||||||
|
}
|
||||||
|
|
||||||
const key = String(clsElement.key);
|
const key = String(clsElement.key);
|
||||||
storageKey = storageKey || String(clsElement.key);
|
const storageKey = options.key || String(clsElement.key);
|
||||||
const initVal = clsElement.initializer
|
const initVal = clsElement.initializer
|
||||||
? clsElement.initializer()
|
? clsElement.initializer()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
storage.addFromStorage(storageKey);
|
storageInstance.addFromStorage(storageKey);
|
||||||
|
|
||||||
const subscribeChanges = (el: ReactiveElement): UnsubscribeFunc =>
|
const subscribeChanges =
|
||||||
storage.subscribeChanges(storageKey!, (oldValue) => {
|
options.subscribe !== false
|
||||||
|
? (el: ReactiveElement): UnsubscribeFunc =>
|
||||||
|
storageInstance.subscribeChanges(
|
||||||
|
storageKey!,
|
||||||
|
(oldValue, _newValue) => {
|
||||||
el.requestUpdate(clsElement.key, oldValue);
|
el.requestUpdate(clsElement.key, oldValue);
|
||||||
});
|
}
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const getValue = (): any =>
|
const getValue = (): any =>
|
||||||
storage.hasKey(storageKey!) ? storage.getValue(storageKey!) : initVal;
|
storageInstance.hasKey(storageKey!)
|
||||||
|
? storageInstance.getValue(storageKey!)
|
||||||
|
: initVal;
|
||||||
|
|
||||||
const setValue = (el: ReactiveElement, value: any) => {
|
const setValue = (el: ReactiveElement, value: any) => {
|
||||||
let oldValue: unknown | undefined;
|
let oldValue: unknown | undefined;
|
||||||
if (property) {
|
if (options.state) {
|
||||||
oldValue = getValue();
|
oldValue = getValue();
|
||||||
}
|
}
|
||||||
storage.setValue(storageKey!, value);
|
storageInstance.setValue(storageKey!, value);
|
||||||
if (property) {
|
if (options.state) {
|
||||||
el.requestUpdate(clsElement.key, oldValue);
|
el.requestUpdate(clsElement.key, oldValue);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -148,22 +170,23 @@ export const LocalStorage =
|
|||||||
configurable: true,
|
configurable: true,
|
||||||
},
|
},
|
||||||
finisher(cls: typeof ReactiveElement) {
|
finisher(cls: typeof ReactiveElement) {
|
||||||
if (property && subscribe) {
|
if (options.state && options.subscribe) {
|
||||||
const connectedCallback = cls.prototype.connectedCallback;
|
const connectedCallback = cls.prototype.connectedCallback;
|
||||||
const disconnectedCallback = cls.prototype.disconnectedCallback;
|
const disconnectedCallback = cls.prototype.disconnectedCallback;
|
||||||
cls.prototype.connectedCallback = function () {
|
cls.prototype.connectedCallback = function () {
|
||||||
connectedCallback.call(this);
|
connectedCallback.call(this);
|
||||||
this[`__unbsubLocalStorage${key}`] = subscribeChanges(this);
|
this[`__unbsubLocalStorage${key}`] = subscribeChanges?.(this);
|
||||||
};
|
};
|
||||||
cls.prototype.disconnectedCallback = function () {
|
cls.prototype.disconnectedCallback = function () {
|
||||||
disconnectedCallback.call(this);
|
disconnectedCallback.call(this);
|
||||||
this[`__unbsubLocalStorage${key}`]();
|
this[`__unbsubLocalStorage${key}`]?.();
|
||||||
|
this[`__unbsubLocalStorage${key}`] = undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (property) {
|
if (options.state) {
|
||||||
cls.createProperty(clsElement.key, {
|
cls.createProperty(clsElement.key, {
|
||||||
noAccessor: true,
|
noAccessor: true,
|
||||||
...propertyOptions,
|
...options.stateOptions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
@ -1,4 +1,4 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||||
import { html, TemplateResult } from "lit";
|
import { html, TemplateResult } from "lit";
|
||||||
import { until } from "lit/directives/until";
|
import { until } from "lit/directives/until";
|
||||||
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||||
@ -20,6 +20,7 @@ export const computeAttributeValueDisplay = (
|
|||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
stateObj: HassEntity,
|
stateObj: HassEntity,
|
||||||
locale: FrontendLocaleData,
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig,
|
||||||
entities: HomeAssistant["entities"],
|
entities: HomeAssistant["entities"],
|
||||||
attribute: string,
|
attribute: string,
|
||||||
value?: any
|
value?: any
|
||||||
@ -59,14 +60,14 @@ export const computeAttributeValueDisplay = (
|
|||||||
if (isTimestamp(attributeValue)) {
|
if (isTimestamp(attributeValue)) {
|
||||||
const date = new Date(attributeValue);
|
const date = new Date(attributeValue);
|
||||||
if (checkValidDate(date)) {
|
if (checkValidDate(date)) {
|
||||||
return formatDateTimeWithSeconds(date, locale);
|
return formatDateTimeWithSeconds(date, locale, config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Value was not a timestamp, so only do date formatting
|
// Value was not a timestamp, so only do date formatting
|
||||||
const date = new Date(attributeValue);
|
const date = new Date(attributeValue);
|
||||||
if (checkValidDate(date)) {
|
if (checkValidDate(date)) {
|
||||||
return formatDate(date, locale);
|
return formatDate(date, locale, config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,6 +93,7 @@ export const computeAttributeValueDisplay = (
|
|||||||
localize,
|
localize,
|
||||||
stateObj,
|
stateObj,
|
||||||
locale,
|
locale,
|
||||||
|
config,
|
||||||
entities,
|
entities,
|
||||||
attribute,
|
attribute,
|
||||||
item
|
item
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||||
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
import { FrontendLocaleData, TimeZone } from "../../data/translation";
|
||||||
import {
|
import {
|
||||||
updateIsInstallingFromAttributes,
|
updateIsInstallingFromAttributes,
|
||||||
UPDATE_SUPPORT_PROGRESS,
|
UPDATE_SUPPORT_PROGRESS,
|
||||||
@ -28,12 +28,14 @@ export const computeStateDisplaySingleEntity = (
|
|||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
stateObj: HassEntity,
|
stateObj: HassEntity,
|
||||||
locale: FrontendLocaleData,
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig,
|
||||||
entity: EntityRegistryDisplayEntry | undefined,
|
entity: EntityRegistryDisplayEntry | undefined,
|
||||||
state?: string
|
state?: string
|
||||||
): string =>
|
): string =>
|
||||||
computeStateDisplayFromEntityAttributes(
|
computeStateDisplayFromEntityAttributes(
|
||||||
localize,
|
localize,
|
||||||
locale,
|
locale,
|
||||||
|
config,
|
||||||
entity,
|
entity,
|
||||||
stateObj.entity_id,
|
stateObj.entity_id,
|
||||||
stateObj.attributes,
|
stateObj.attributes,
|
||||||
@ -44,6 +46,7 @@ export const computeStateDisplay = (
|
|||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
stateObj: HassEntity,
|
stateObj: HassEntity,
|
||||||
locale: FrontendLocaleData,
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig,
|
||||||
entities: HomeAssistant["entities"],
|
entities: HomeAssistant["entities"],
|
||||||
state?: string
|
state?: string
|
||||||
): string => {
|
): string => {
|
||||||
@ -54,6 +57,7 @@ export const computeStateDisplay = (
|
|||||||
return computeStateDisplayFromEntityAttributes(
|
return computeStateDisplayFromEntityAttributes(
|
||||||
localize,
|
localize,
|
||||||
locale,
|
locale,
|
||||||
|
config,
|
||||||
entity,
|
entity,
|
||||||
stateObj.entity_id,
|
stateObj.entity_id,
|
||||||
stateObj.attributes,
|
stateObj.attributes,
|
||||||
@ -64,6 +68,7 @@ export const computeStateDisplay = (
|
|||||||
export const computeStateDisplayFromEntityAttributes = (
|
export const computeStateDisplayFromEntityAttributes = (
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
locale: FrontendLocaleData,
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig,
|
||||||
entity: EntityRegistryDisplayEntry | undefined,
|
entity: EntityRegistryDisplayEntry | undefined,
|
||||||
entityId: string,
|
entityId: string,
|
||||||
attributes: any,
|
attributes: any,
|
||||||
@ -119,29 +124,40 @@ export const computeStateDisplayFromEntityAttributes = (
|
|||||||
|
|
||||||
if (domain === "datetime") {
|
if (domain === "datetime") {
|
||||||
const time = new Date(state);
|
const time = new Date(state);
|
||||||
return formatDateTime(time, locale);
|
return formatDateTime(time, locale, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (["date", "input_datetime", "time"].includes(domain)) {
|
if (["date", "input_datetime", "time"].includes(domain)) {
|
||||||
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
|
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
|
||||||
// Attributes aren't available, we have to use `state`.
|
// Attributes aren't available, we have to use `state`.
|
||||||
|
|
||||||
|
// These are timezone agnostic, so we should NOT use the system timezone here.
|
||||||
try {
|
try {
|
||||||
const components = state.split(" ");
|
const components = state.split(" ");
|
||||||
if (components.length === 2) {
|
if (components.length === 2) {
|
||||||
// Date and time.
|
// Date and time.
|
||||||
return formatDateTime(new Date(components.join("T")), locale);
|
return formatDateTime(
|
||||||
|
new Date(components.join("T")),
|
||||||
|
{ ...locale, time_zone: TimeZone.local },
|
||||||
|
config
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (components.length === 1) {
|
if (components.length === 1) {
|
||||||
if (state.includes("-")) {
|
if (state.includes("-")) {
|
||||||
// Date only.
|
// Date only.
|
||||||
return formatDate(new Date(`${state}T00:00`), locale);
|
return formatDate(
|
||||||
|
new Date(`${state}T00:00`),
|
||||||
|
{ ...locale, time_zone: TimeZone.local },
|
||||||
|
config
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (state.includes(":")) {
|
if (state.includes(":")) {
|
||||||
// Time only.
|
// Time only.
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
return formatTime(
|
return formatTime(
|
||||||
new Date(`${now.toISOString().split("T")[0]}T${state}`),
|
new Date(`${now.toISOString().split("T")[0]}T${state}`),
|
||||||
locale
|
{ ...locale, time_zone: TimeZone.local },
|
||||||
|
config
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,12 +169,6 @@ export const computeStateDisplayFromEntityAttributes = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domain === "humidifier") {
|
|
||||||
if (state === "on" && attributes.humidity) {
|
|
||||||
return `${attributes.humidity} %`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// `counter` `number` and `input_number` domains do not have a unit of measurement but should still use `formatNumber`
|
// `counter` `number` and `input_number` domains do not have a unit of measurement but should still use `formatNumber`
|
||||||
if (
|
if (
|
||||||
domain === "counter" ||
|
domain === "counter" ||
|
||||||
@ -175,11 +185,13 @@ export const computeStateDisplayFromEntityAttributes = (
|
|||||||
|
|
||||||
// state is a timestamp
|
// state is a timestamp
|
||||||
if (
|
if (
|
||||||
["button", "input_button", "scene", "stt", "tts"].includes(domain) ||
|
["button", "image", "input_button", "scene", "stt", "tts"].includes(
|
||||||
|
domain
|
||||||
|
) ||
|
||||||
(domain === "sensor" && attributes.device_class === "timestamp")
|
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
return formatDateTime(new Date(state), locale);
|
return formatDateTime(new Date(state), locale, config);
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
mdiCheckCircleOutline,
|
mdiCheckCircleOutline,
|
||||||
mdiClock,
|
mdiClock,
|
||||||
mdiCloseCircleOutline,
|
mdiCloseCircleOutline,
|
||||||
|
mdiCrosshairsQuestion,
|
||||||
mdiFan,
|
mdiFan,
|
||||||
mdiFanOff,
|
mdiFanOff,
|
||||||
mdiGestureTapButton,
|
mdiGestureTapButton,
|
||||||
@ -31,6 +32,7 @@ import {
|
|||||||
mdiPowerPlugOff,
|
mdiPowerPlugOff,
|
||||||
mdiRestart,
|
mdiRestart,
|
||||||
mdiRobot,
|
mdiRobot,
|
||||||
|
mdiRobotConfused,
|
||||||
mdiRobotOff,
|
mdiRobotOff,
|
||||||
mdiSpeaker,
|
mdiSpeaker,
|
||||||
mdiSpeakerOff,
|
mdiSpeakerOff,
|
||||||
@ -91,13 +93,19 @@ export const domainIconWithoutDefault = (
|
|||||||
return alarmPanelIcon(compareState);
|
return alarmPanelIcon(compareState);
|
||||||
|
|
||||||
case "automation":
|
case "automation":
|
||||||
return compareState === "off" ? mdiRobotOff : mdiRobot;
|
return compareState === "unavailable"
|
||||||
|
? mdiRobotConfused
|
||||||
|
: compareState === "off"
|
||||||
|
? mdiRobotOff
|
||||||
|
: mdiRobot;
|
||||||
|
|
||||||
case "binary_sensor":
|
case "binary_sensor":
|
||||||
return binarySensorIcon(compareState, stateObj);
|
return binarySensorIcon(compareState, stateObj);
|
||||||
|
|
||||||
case "button":
|
case "button":
|
||||||
switch (stateObj?.attributes.device_class) {
|
switch (stateObj?.attributes.device_class) {
|
||||||
|
case "identify":
|
||||||
|
return mdiCrosshairsQuestion;
|
||||||
case "restart":
|
case "restart":
|
||||||
return mdiRestart;
|
return mdiRestart;
|
||||||
case "update":
|
case "update":
|
||||||
|
@ -30,6 +30,7 @@ export const FIXED_DOMAIN_STATES = {
|
|||||||
lock: ["jammed", "locked", "locking", "unlocked", "unlocking"],
|
lock: ["jammed", "locked", "locking", "unlocked", "unlocking"],
|
||||||
media_player: ["idle", "off", "paused", "playing", "standby"],
|
media_player: ["idle", "off", "paused", "playing", "standby"],
|
||||||
person: ["home", "not_home"],
|
person: ["home", "not_home"],
|
||||||
|
plant: ["ok", "problem"],
|
||||||
remote: ["on", "off"],
|
remote: ["on", "off"],
|
||||||
scene: [],
|
scene: [],
|
||||||
schedule: ["on", "off"],
|
schedule: ["on", "off"],
|
||||||
@ -102,7 +103,15 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
|||||||
frontend_stream_type: ["hls", "web_rtc"],
|
frontend_stream_type: ["hls", "web_rtc"],
|
||||||
},
|
},
|
||||||
climate: {
|
climate: {
|
||||||
hvac_action: ["off", "idle", "heating", "cooling", "drying", "fan"],
|
hvac_action: [
|
||||||
|
"off",
|
||||||
|
"idle",
|
||||||
|
"preheating",
|
||||||
|
"heating",
|
||||||
|
"cooling",
|
||||||
|
"drying",
|
||||||
|
"fan",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
cover: {
|
cover: {
|
||||||
device_class: [
|
device_class: [
|
||||||
@ -126,6 +135,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
|||||||
},
|
},
|
||||||
humidifier: {
|
humidifier: {
|
||||||
device_class: ["humidifier", "dehumidifier"],
|
device_class: ["humidifier", "dehumidifier"],
|
||||||
|
action: ["off", "idle", "humidifying", "drying"],
|
||||||
},
|
},
|
||||||
media_player: {
|
media_player: {
|
||||||
device_class: ["tv", "speaker", "receiver"],
|
device_class: ["tv", "speaker", "receiver"],
|
||||||
|
@ -110,3 +110,15 @@ export const stateColorProperties = (
|
|||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const stateColorBrightness = (stateObj: HassEntity): string => {
|
||||||
|
if (
|
||||||
|
stateObj.attributes.brightness &&
|
||||||
|
computeDomain(stateObj.entity_id) !== "plant"
|
||||||
|
) {
|
||||||
|
// lowest brightness will be around 50% (that's pretty dark)
|
||||||
|
const brightness = stateObj.attributes.brightness;
|
||||||
|
return `brightness(${(brightness + 245) / 5}%)`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
@ -17,7 +17,7 @@ export const stripPrefixFromEntityName = (
|
|||||||
|
|
||||||
if (lowerCasedEntityName.startsWith(lowerCasedPrefixWithSuffix)) {
|
if (lowerCasedEntityName.startsWith(lowerCasedPrefixWithSuffix)) {
|
||||||
const newName = entityName.substring(lowerCasedPrefixWithSuffix.length);
|
const newName = entityName.substring(lowerCasedPrefixWithSuffix.length);
|
||||||
|
if (newName.length) {
|
||||||
// If first word already has an upper case letter (e.g. from brand name)
|
// If first word already has an upper case letter (e.g. from brand name)
|
||||||
// leave as-is, otherwise capitalize the first word.
|
// leave as-is, otherwise capitalize the first word.
|
||||||
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
|
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
|
||||||
@ -25,6 +25,7 @@ export const stripPrefixFromEntityName = (
|
|||||||
: newName[0].toUpperCase() + newName.slice(1);
|
: newName[0].toUpperCase() + newName.slice(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
19
src/common/translations/auto_case_noun.ts
Normal file
19
src/common/translations/auto_case_noun.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// In a few languages nouns are always capitalized. This helper
|
||||||
|
// indicates if for a given language that is the case.
|
||||||
|
|
||||||
|
import { capitalizeFirstLetter } from "../string/capitalize-first-letter";
|
||||||
|
|
||||||
|
export const useCapitalizedNouns = (language: string): boolean => {
|
||||||
|
switch (language) {
|
||||||
|
case "de":
|
||||||
|
case "lb":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const autoCaseNoun = (noun: string, language: string): string =>
|
||||||
|
useCapitalizedNouns(language)
|
||||||
|
? capitalizeFirstLetter(noun)
|
||||||
|
: noun.toLocaleLowerCase(language);
|
@ -1,10 +1,12 @@
|
|||||||
import { addDays, startOfWeek } from "date-fns";
|
import { addDays, startOfWeek } from "date-fns";
|
||||||
|
import { HassConfig } from "home-assistant-js-websocket";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
import { FrontendLocaleData } from "../../data/translation";
|
||||||
import { formatDateWeekday } from "../datetime/format_date";
|
import { formatDateWeekday } from "../datetime/format_date";
|
||||||
|
|
||||||
export const dayNames = memoizeOne((locale: FrontendLocaleData): string[] =>
|
export const dayNames = memoizeOne(
|
||||||
|
(locale: FrontendLocaleData, config: HassConfig): string[] =>
|
||||||
Array.from({ length: 7 }, (_, d) =>
|
Array.from({ length: 7 }, (_, d) =>
|
||||||
formatDateWeekday(addDays(startOfWeek(new Date()), d), locale)
|
formatDateWeekday(addDays(startOfWeek(new Date()), d), locale, config)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { addMonths, startOfYear } from "date-fns";
|
import { addMonths, startOfYear } from "date-fns";
|
||||||
|
import { HassConfig } from "home-assistant-js-websocket";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
import { FrontendLocaleData } from "../../data/translation";
|
||||||
import { formatDateMonth } from "../datetime/format_date";
|
import { formatDateMonth } from "../datetime/format_date";
|
||||||
|
|
||||||
export const monthNames = memoizeOne((locale: FrontendLocaleData): string[] =>
|
export const monthNames = memoizeOne(
|
||||||
|
(locale: FrontendLocaleData, config: HassConfig): string[] =>
|
||||||
Array.from({ length: 12 }, (_, m) =>
|
Array.from({ length: 12 }, (_, m) =>
|
||||||
formatDateMonth(addMonths(startOfYear(new Date()), m), locale)
|
formatDateMonth(addMonths(startOfYear(new Date()), m), locale, config)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -80,33 +80,89 @@ _adapters._date.override({
|
|||||||
format: function (time, fmt: keyof typeof FORMATS) {
|
format: function (time, fmt: keyof typeof FORMATS) {
|
||||||
switch (fmt) {
|
switch (fmt) {
|
||||||
case "datetime":
|
case "datetime":
|
||||||
return formatDateTime(new Date(time), this.options.locale);
|
return formatDateTime(
|
||||||
|
new Date(time),
|
||||||
|
this.options.locale,
|
||||||
|
this.options.config
|
||||||
|
);
|
||||||
case "datetimeseconds":
|
case "datetimeseconds":
|
||||||
return formatDateTimeWithSeconds(new Date(time), this.options.locale);
|
return formatDateTimeWithSeconds(
|
||||||
|
new Date(time),
|
||||||
|
this.options.locale,
|
||||||
|
this.options.config
|
||||||
|
);
|
||||||
case "millisecond":
|
case "millisecond":
|
||||||
return formatTimeWithSeconds(new Date(time), this.options.locale);
|
return formatTimeWithSeconds(
|
||||||
|
new Date(time),
|
||||||
|
this.options.locale,
|
||||||
|
this.options.config
|
||||||
|
);
|
||||||
case "second":
|
case "second":
|
||||||
return formatTimeWithSeconds(new Date(time), this.options.locale);
|
return formatTimeWithSeconds(
|
||||||
|
new Date(time),
|
||||||
|
this.options.locale,
|
||||||
|
this.options.config
|
||||||
|
);
|
||||||
case "minute":
|
case "minute":
|
||||||
return formatTime(new Date(time), this.options.locale);
|
return formatTime(
|
||||||
|
new Date(time),
|
||||||
|
this.options.locale,
|
||||||
|
this.options.config
|
||||||
|
);
|
||||||
case "hour":
|
case "hour":
|
||||||
return formatTime(new Date(time), this.options.locale);
|
return formatTime(
|
||||||
|
new Date(time),
|
||||||
|
this.options.locale,
|
||||||
|
this.options.config
|
||||||
|
);
|
||||||
case "weekday":
|
case "weekday":
|
||||||
return formatDateWeekdayDay(new Date(time), this.options.locale);
|
return formatDateWeekdayDay(
|
||||||
|
new Date(time),
|
||||||
|
this.options.locale,
|
||||||
|
this.options.config
|
||||||
|
);
|
||||||
case "date":
|
case "date":
|
||||||
return formatDate(new Date(time), this.options.locale);
|
return formatDate(
|
||||||
|
new Date(time),
|
||||||
|
this.options.locale,
|
||||||
|
this.options.config
|
||||||
|
);
|
||||||
case "day":
|
case "day":
|
||||||
return formatDateShort(new Date(time), this.options.locale);
|
return formatDateShort(
|
||||||
|
new Date(time),
|
||||||
|
this.options.locale,
|
||||||
|
this.options.config
|
||||||
|
);
|
||||||
case "week":
|
case "week":
|
||||||
return formatDate(new Date(time), this.options.locale);
|
return formatDate(
|
||||||
|
new Date(time),
|
||||||
|
this.options.locale,
|
||||||
|
this.options.config
|
||||||
|
);
|
||||||
case "month":
|
case "month":
|
||||||
return formatDateMonth(new Date(time), this.options.locale);
|
return formatDateMonth(
|
||||||
|
new Date(time),
|
||||||
|
this.options.locale,
|
||||||
|
this.options.config
|
||||||
|
);
|
||||||
case "monthyear":
|
case "monthyear":
|
||||||
return formatDateMonthYear(new Date(time), this.options.locale);
|
return formatDateMonthYear(
|
||||||
|
new Date(time),
|
||||||
|
this.options.locale,
|
||||||
|
this.options.config
|
||||||
|
);
|
||||||
case "quarter":
|
case "quarter":
|
||||||
return formatDate(new Date(time), this.options.locale);
|
return formatDate(
|
||||||
|
new Date(time),
|
||||||
|
this.options.locale,
|
||||||
|
this.options.config
|
||||||
|
);
|
||||||
case "year":
|
case "year":
|
||||||
return formatDateYear(new Date(time), this.options.locale);
|
return formatDateYear(
|
||||||
|
new Date(time),
|
||||||
|
this.options.locale,
|
||||||
|
this.options.config
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,8 @@ class StateHistoryChartLine extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public showNames = true;
|
@property({ type: Boolean }) public showNames = true;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public startTime!: Date;
|
||||||
|
|
||||||
@property({ attribute: false }) public endTime!: Date;
|
@property({ attribute: false }) public endTime!: Date;
|
||||||
|
|
||||||
@property({ type: Number }) public paddingYAxis = 0;
|
@property({ type: Number }) public paddingYAxis = 0;
|
||||||
@ -57,7 +59,12 @@ class StateHistoryChartLine extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public willUpdate(changedProps: PropertyValues) {
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
if (!this.hasUpdated || changedProps.has("showNames")) {
|
if (
|
||||||
|
!this.hasUpdated ||
|
||||||
|
changedProps.has("showNames") ||
|
||||||
|
changedProps.has("startTime") ||
|
||||||
|
changedProps.has("endTime")
|
||||||
|
) {
|
||||||
this._chartOptions = {
|
this._chartOptions = {
|
||||||
parsing: false,
|
parsing: false,
|
||||||
animation: false,
|
animation: false,
|
||||||
@ -71,8 +78,10 @@ class StateHistoryChartLine extends LitElement {
|
|||||||
adapters: {
|
adapters: {
|
||||||
date: {
|
date: {
|
||||||
locale: this.hass.locale,
|
locale: this.hass.locale,
|
||||||
|
config: this.hass.config,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
suggestedMin: this.startTime,
|
||||||
suggestedMax: this.endTime,
|
suggestedMax: this.endTime,
|
||||||
ticks: {
|
ticks: {
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
@ -145,6 +154,8 @@ class StateHistoryChartLine extends LitElement {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
changedProps.has("data") ||
|
changedProps.has("data") ||
|
||||||
|
changedProps.has("startTime") ||
|
||||||
|
changedProps.has("endTime") ||
|
||||||
this._chartTime <
|
this._chartTime <
|
||||||
new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES)
|
new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES)
|
||||||
) {
|
) {
|
||||||
@ -375,6 +386,9 @@ class StateHistoryChartLine extends LitElement {
|
|||||||
lastNullDate = date;
|
lastNullDate = date;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (lastNullDate !== null) {
|
||||||
|
pushData(lastNullDate, [null]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add an entry for final values
|
// Add an entry for final values
|
||||||
|
@ -98,6 +98,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||||||
adapters: {
|
adapters: {
|
||||||
date: {
|
date: {
|
||||||
locale: this.hass.locale,
|
locale: this.hass.locale,
|
||||||
|
config: this.hass.config,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
suggestedMin: this.startTime,
|
suggestedMin: this.startTime,
|
||||||
@ -181,8 +182,16 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
d.label || "",
|
d.label || "",
|
||||||
formatDateTimeWithSeconds(d.start, this.hass.locale),
|
formatDateTimeWithSeconds(
|
||||||
formatDateTimeWithSeconds(d.end, this.hass.locale),
|
d.start,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config
|
||||||
|
),
|
||||||
|
formatDateTimeWithSeconds(
|
||||||
|
d.end,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config
|
||||||
|
),
|
||||||
formattedDuration,
|
formattedDuration,
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
@ -52,8 +52,12 @@ export class StateHistoryCharts extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public endTime?: Date;
|
@property({ attribute: false }) public endTime?: Date;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public startTime?: Date;
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
|
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
|
||||||
|
|
||||||
|
@property() public hoursToShow?: number;
|
||||||
|
|
||||||
@property({ type: Boolean }) public showNames = true;
|
@property({ type: Boolean }) public showNames = true;
|
||||||
|
|
||||||
@property({ type: Boolean }) public isLoadingData = false;
|
@property({ type: Boolean }) public isLoadingData = false;
|
||||||
@ -95,13 +99,24 @@ export class StateHistoryCharts extends LitElement {
|
|||||||
this._computedEndTime =
|
this._computedEndTime =
|
||||||
this.upToNow || !this.endTime || this.endTime > now ? now : this.endTime;
|
this.upToNow || !this.endTime || this.endTime > now ? now : this.endTime;
|
||||||
|
|
||||||
|
if (this.startTime) {
|
||||||
|
this._computedStartTime = this.startTime;
|
||||||
|
} else if (this.hoursToShow) {
|
||||||
|
this._computedStartTime = new Date(
|
||||||
|
new Date().getTime() - 60 * 60 * this.hoursToShow * 1000
|
||||||
|
);
|
||||||
|
} else {
|
||||||
this._computedStartTime = new Date(
|
this._computedStartTime = new Date(
|
||||||
this.historyData.timeline.reduce(
|
this.historyData.timeline.reduce(
|
||||||
(minTime, stateInfo) =>
|
(minTime, stateInfo) =>
|
||||||
Math.min(minTime, new Date(stateInfo.data[0].last_changed).getTime()),
|
Math.min(
|
||||||
|
minTime,
|
||||||
|
new Date(stateInfo.data[0].last_changed).getTime()
|
||||||
|
),
|
||||||
new Date().getTime()
|
new Date().getTime()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const combinedItems = this.historyData.timeline.length
|
const combinedItems = this.historyData.timeline.length
|
||||||
? (this.virtualize
|
? (this.virtualize
|
||||||
@ -142,6 +157,7 @@ export class StateHistoryCharts extends LitElement {
|
|||||||
.data=${item.data}
|
.data=${item.data}
|
||||||
.identifier=${item.identifier}
|
.identifier=${item.identifier}
|
||||||
.showNames=${this.showNames}
|
.showNames=${this.showNames}
|
||||||
|
.startTime=${this._computedStartTime}
|
||||||
.endTime=${this._computedEndTime}
|
.endTime=${this._computedEndTime}
|
||||||
.paddingYAxis=${this._maxYWidth}
|
.paddingYAxis=${this._maxYWidth}
|
||||||
.names=${this.names}
|
.names=${this.names}
|
||||||
|
@ -146,6 +146,7 @@ class StatisticsChart extends LitElement {
|
|||||||
adapters: {
|
adapters: {
|
||||||
date: {
|
date: {
|
||||||
locale: this.hass.locale,
|
locale: this.hass.locale,
|
||||||
|
config: this.hass.config,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
@ -165,7 +166,7 @@ class StatisticsChart extends LitElement {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
beginAtZero: false,
|
beginAtZero: this.chartType === "bar",
|
||||||
ticks: {
|
ticks: {
|
||||||
maxTicksLimit: 7,
|
maxTicksLimit: 7,
|
||||||
},
|
},
|
||||||
|
@ -349,6 +349,7 @@ export class HaDataTable extends LitElement {
|
|||||||
class="mdc-data-table__content scroller ha-scrollbar"
|
class="mdc-data-table__content scroller ha-scrollbar"
|
||||||
@scroll=${this._saveScrollPos}
|
@scroll=${this._saveScrollPos}
|
||||||
.items=${this._items}
|
.items=${this._items}
|
||||||
|
.keyFunction=${this._keyFunction}
|
||||||
.renderItem=${this._renderRow}
|
.renderItem=${this._renderRow}
|
||||||
></lit-virtualizer>
|
></lit-virtualizer>
|
||||||
`}
|
`}
|
||||||
@ -357,6 +358,8 @@ export class HaDataTable extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _keyFunction = (row: DataTableRowData) => row[this.id] || row;
|
||||||
|
|
||||||
private _renderRow = (row: DataTableRowData, index: number) => {
|
private _renderRow = (row: DataTableRowData, index: number) => {
|
||||||
// not sure how this happens...
|
// not sure how this happens...
|
||||||
if (!row) {
|
if (!row) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Remote, wrap } from "comlink";
|
import { Remote, wrap } from "comlink";
|
||||||
import type { Api } from "./sort_filter_worker";
|
import type { Api } from "./sort-filter-worker";
|
||||||
|
|
||||||
type FilterDataType = Api["filterData"];
|
type FilterDataType = Api["filterData"];
|
||||||
type FilterDataParamTypes = Parameters<FilterDataType>;
|
type FilterDataParamTypes = Parameters<FilterDataType>;
|
||||||
@ -9,27 +9,28 @@ type SortDataParamTypes = Parameters<SortDataType>;
|
|||||||
|
|
||||||
let worker: Remote<Api> | undefined;
|
let worker: Remote<Api> | undefined;
|
||||||
|
|
||||||
|
const getWorker = () => {
|
||||||
|
if (!worker) {
|
||||||
|
worker = wrap(
|
||||||
|
new Worker(
|
||||||
|
/* webpackChunkName: "sort-filter-worker" */
|
||||||
|
new URL("./sort-filter-worker", import.meta.url)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return worker;
|
||||||
|
};
|
||||||
|
|
||||||
export const filterData = (
|
export const filterData = (
|
||||||
data: FilterDataParamTypes[0],
|
data: FilterDataParamTypes[0],
|
||||||
columns: FilterDataParamTypes[1],
|
columns: FilterDataParamTypes[1],
|
||||||
filter: FilterDataParamTypes[2]
|
filter: FilterDataParamTypes[2]
|
||||||
): Promise<ReturnType<FilterDataType>> => {
|
): Promise<ReturnType<FilterDataType>> =>
|
||||||
if (!worker) {
|
getWorker().filterData(data, columns, filter);
|
||||||
worker = wrap(new Worker(new URL("./sort_filter_worker", import.meta.url)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return worker.filterData(data, columns, filter);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const sortData = (
|
export const sortData = (
|
||||||
data: SortDataParamTypes[0],
|
data: SortDataParamTypes[0],
|
||||||
columns: SortDataParamTypes[1],
|
columns: SortDataParamTypes[1],
|
||||||
direction: SortDataParamTypes[2],
|
direction: SortDataParamTypes[2],
|
||||||
sortColumn: SortDataParamTypes[3]
|
sortColumn: SortDataParamTypes[3]
|
||||||
): Promise<ReturnType<SortDataType>> => {
|
): Promise<ReturnType<SortDataType>> =>
|
||||||
if (!worker) {
|
getWorker().sortData(data, columns, direction, sortColumn);
|
||||||
worker = wrap(new Worker(new URL("./sort_filter_worker", import.meta.url)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return worker.sortData(data, columns, direction, sortColumn);
|
|
||||||
};
|
|
||||||
|
@ -26,6 +26,10 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
|||||||
import { ValueChangedEvent, HomeAssistant } from "../../types";
|
import { ValueChangedEvent, HomeAssistant } from "../../types";
|
||||||
import "../ha-combo-box";
|
import "../ha-combo-box";
|
||||||
import type { HaComboBox } from "../ha-combo-box";
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
|
import {
|
||||||
|
fuzzyFilterSort,
|
||||||
|
ScorableTextItem,
|
||||||
|
} from "../../common/string/filter/sequence-matching";
|
||||||
|
|
||||||
interface Device {
|
interface Device {
|
||||||
name: string;
|
name: string;
|
||||||
@ -33,6 +37,8 @@ interface Device {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScorableDevice = ScorableTextItem & Device;
|
||||||
|
|
||||||
export type HaDevicePickerDeviceFilterFunc = (
|
export type HaDevicePickerDeviceFilterFunc = (
|
||||||
device: DeviceRegistryEntry
|
device: DeviceRegistryEntry
|
||||||
) => boolean;
|
) => boolean;
|
||||||
@ -119,13 +125,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
deviceFilter: this["deviceFilter"],
|
deviceFilter: this["deviceFilter"],
|
||||||
entityFilter: this["entityFilter"],
|
entityFilter: this["entityFilter"],
|
||||||
excludeDevices: this["excludeDevices"]
|
excludeDevices: this["excludeDevices"]
|
||||||
): Device[] => {
|
): ScorableDevice[] => {
|
||||||
if (!devices.length) {
|
if (!devices.length) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: "no_devices",
|
id: "no_devices",
|
||||||
area: "",
|
area: "",
|
||||||
name: this.hass.localize("ui.components.device-picker.no_devices"),
|
name: this.hass.localize("ui.components.device-picker.no_devices"),
|
||||||
|
strings: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -235,6 +242,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
device.area_id && areaLookup[device.area_id]
|
device.area_id && areaLookup[device.area_id]
|
||||||
? areaLookup[device.area_id].name
|
? areaLookup[device.area_id].name
|
||||||
: this.hass.localize("ui.components.device-picker.no_area"),
|
: this.hass.localize("ui.components.device-picker.no_area"),
|
||||||
|
strings: [device.name || ""],
|
||||||
}));
|
}));
|
||||||
if (!outputDevices.length) {
|
if (!outputDevices.length) {
|
||||||
return [
|
return [
|
||||||
@ -242,6 +250,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
id: "no_devices",
|
id: "no_devices",
|
||||||
area: "",
|
area: "",
|
||||||
name: this.hass.localize("ui.components.device-picker.no_match"),
|
name: this.hass.localize("ui.components.device-picker.no_match"),
|
||||||
|
strings: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -284,7 +293,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
(this._init && changedProps.has("_opened") && this._opened)
|
(this._init && changedProps.has("_opened") && this._opened)
|
||||||
) {
|
) {
|
||||||
this._init = true;
|
this._init = true;
|
||||||
(this.comboBox as any).items = this._getDevices(
|
const devices = this._getDevices(
|
||||||
this.devices!,
|
this.devices!,
|
||||||
this.areas!,
|
this.areas!,
|
||||||
this.entities!,
|
this.entities!,
|
||||||
@ -295,6 +304,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
this.entityFilter,
|
this.entityFilter,
|
||||||
this.excludeDevices
|
this.excludeDevices
|
||||||
);
|
);
|
||||||
|
this.comboBox.items = devices;
|
||||||
|
this.comboBox.filteredItems = devices;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,6 +325,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
item-label-path="name"
|
item-label-path="name"
|
||||||
@opened-changed=${this._openedChanged}
|
@opened-changed=${this._openedChanged}
|
||||||
@value-changed=${this._deviceChanged}
|
@value-changed=${this._deviceChanged}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
></ha-combo-box>
|
></ha-combo-box>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -322,6 +334,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
|||||||
return this.value || "";
|
return this.value || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _filterChanged(ev: CustomEvent): void {
|
||||||
|
const target = ev.target as HaComboBox;
|
||||||
|
const filterString = ev.detail.value.toLowerCase();
|
||||||
|
target.filteredItems = filterString.length
|
||||||
|
? fuzzyFilterSort<ScorableDevice>(filterString, target.items || [])
|
||||||
|
: target.items;
|
||||||
|
}
|
||||||
|
|
||||||
private _deviceChanged(ev: ValueChangedEvent<string>) {
|
private _deviceChanged(ev: ValueChangedEvent<string>) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
let newValue = ev.detail.value;
|
let newValue = ev.detail.value;
|
||||||
|
@ -7,15 +7,19 @@ import memoizeOne from "memoize-one";
|
|||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { computeDomain } from "../../common/entity/compute_domain";
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
import {
|
||||||
|
fuzzyFilterSort,
|
||||||
|
ScorableTextItem,
|
||||||
|
} from "../../common/string/filter/sequence-matching";
|
||||||
import { ValueChangedEvent, HomeAssistant } from "../../types";
|
import { ValueChangedEvent, HomeAssistant } from "../../types";
|
||||||
import "../ha-combo-box";
|
import "../ha-combo-box";
|
||||||
import type { HaComboBox } from "../ha-combo-box";
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
import "../ha-icon-button";
|
import "../ha-icon-button";
|
||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "./state-badge";
|
import "./state-badge";
|
||||||
|
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||||
|
|
||||||
interface HassEntityWithCachedName extends HassEntity {
|
interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
|
||||||
friendly_name: string;
|
friendly_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,6 +163,7 @@ export class HaEntityPicker extends LitElement {
|
|||||||
),
|
),
|
||||||
icon: "mdi:magnify",
|
icon: "mdi:magnify",
|
||||||
},
|
},
|
||||||
|
strings: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -169,10 +174,14 @@ export class HaEntityPicker extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return entityIds
|
return entityIds
|
||||||
.map((key) => ({
|
.map((key) => {
|
||||||
|
const friendly_name = computeStateName(hass!.states[key]) || key;
|
||||||
|
return {
|
||||||
...hass!.states[key],
|
...hass!.states[key],
|
||||||
friendly_name: computeStateName(hass!.states[key]) || key,
|
friendly_name,
|
||||||
}))
|
strings: [key, friendly_name],
|
||||||
|
};
|
||||||
|
})
|
||||||
.sort((entityA, entityB) =>
|
.sort((entityA, entityB) =>
|
||||||
caseInsensitiveStringCompare(
|
caseInsensitiveStringCompare(
|
||||||
entityA.friendly_name,
|
entityA.friendly_name,
|
||||||
@ -201,10 +210,14 @@ export class HaEntityPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
states = entityIds
|
states = entityIds
|
||||||
.map((key) => ({
|
.map((key) => {
|
||||||
|
const friendly_name = computeStateName(hass!.states[key]) || key;
|
||||||
|
return {
|
||||||
...hass!.states[key],
|
...hass!.states[key],
|
||||||
friendly_name: computeStateName(hass!.states[key]) || key,
|
friendly_name,
|
||||||
}))
|
strings: [key, friendly_name],
|
||||||
|
};
|
||||||
|
})
|
||||||
.sort((entityA, entityB) =>
|
.sort((entityA, entityB) =>
|
||||||
caseInsensitiveStringCompare(
|
caseInsensitiveStringCompare(
|
||||||
entityA.friendly_name,
|
entityA.friendly_name,
|
||||||
@ -260,6 +273,7 @@ export class HaEntityPicker extends LitElement {
|
|||||||
),
|
),
|
||||||
icon: "mdi:magnify",
|
icon: "mdi:magnify",
|
||||||
},
|
},
|
||||||
|
strings: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -293,7 +307,7 @@ export class HaEntityPicker extends LitElement {
|
|||||||
this.excludeEntities
|
this.excludeEntities
|
||||||
);
|
);
|
||||||
if (this._initedStates) {
|
if (this._initedStates) {
|
||||||
(this.comboBox as any).filteredItems = this._states;
|
this.comboBox.filteredItems = this._states;
|
||||||
}
|
}
|
||||||
this._initedStates = true;
|
this._initedStates = true;
|
||||||
}
|
}
|
||||||
@ -340,12 +354,11 @@ export class HaEntityPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _filterChanged(ev: CustomEvent): void {
|
private _filterChanged(ev: CustomEvent): void {
|
||||||
|
const target = ev.target as HaComboBox;
|
||||||
const filterString = ev.detail.value.toLowerCase();
|
const filterString = ev.detail.value.toLowerCase();
|
||||||
(this.comboBox as any).filteredItems = this._states.filter(
|
target.filteredItems = filterString.length
|
||||||
(entityState) =>
|
? fuzzyFilterSort<HassEntityWithCachedName>(filterString, this._states)
|
||||||
entityState.entity_id.toLowerCase().includes(filterString) ||
|
: this._states;
|
||||||
computeStateName(entityState).toLowerCase().includes(filterString)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setValue(value: string) {
|
private _setValue(value: string) {
|
||||||
|
@ -62,6 +62,7 @@ class HaEntityStatePicker extends LitElement {
|
|||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
state,
|
state,
|
||||||
this.hass.locale,
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
this.hass.entities,
|
this.hass.entities,
|
||||||
key
|
key
|
||||||
)
|
)
|
||||||
@ -69,6 +70,7 @@ class HaEntityStatePicker extends LitElement {
|
|||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
state,
|
state,
|
||||||
this.hass.locale,
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
this.hass.entities,
|
this.hass.entities,
|
||||||
this.attribute,
|
this.attribute,
|
||||||
key
|
key
|
||||||
|
@ -192,6 +192,7 @@ export class HaStateLabelBadge extends LitElement {
|
|||||||
this.hass!.localize,
|
this.hass!.localize,
|
||||||
entityState,
|
entityState,
|
||||||
this.hass!.locale,
|
this.hass!.locale,
|
||||||
|
this.hass!.config,
|
||||||
this.hass!.entities
|
this.hass!.entities
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,10 @@ import { ifDefined } from "lit/directives/if-defined";
|
|||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
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 { stateColorCss } from "../../common/entity/state_color";
|
import {
|
||||||
|
stateColorCss,
|
||||||
|
stateColorBrightness,
|
||||||
|
} 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 { HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||||
@ -153,8 +156,7 @@ export class StateBadge extends LitElement {
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
console.warn(errorMessage);
|
console.warn(errorMessage);
|
||||||
}
|
}
|
||||||
// lowest brightness will be around 50% (that's pretty dark)
|
iconStyle.filter = stateColorBrightness(stateObj);
|
||||||
iconStyle.filter = `brightness(${(brightness + 245) / 5}%)`;
|
|
||||||
}
|
}
|
||||||
if (stateObj.attributes.hvac_action) {
|
if (stateObj.attributes.hvac_action) {
|
||||||
const hvacAction = stateObj.attributes.hvac_action;
|
const hvacAction = stateObj.attributes.hvac_action;
|
||||||
|
@ -64,7 +64,11 @@ class HaAbsoluteTime extends ReactiveElement {
|
|||||||
if (!this.datetime) {
|
if (!this.datetime) {
|
||||||
this.innerHTML = this.hass.localize("ui.components.absolute_time.never");
|
this.innerHTML = this.hass.localize("ui.components.absolute_time.never");
|
||||||
} else {
|
} else {
|
||||||
this.innerHTML = absoluteTime(new Date(this.datetime), this.hass.locale);
|
this.innerHTML = absoluteTime(
|
||||||
|
new Date(this.datetime),
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,10 @@ import { classMap } from "lit/directives/class-map";
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
|
import {
|
||||||
|
fuzzyFilterSort,
|
||||||
|
ScorableTextItem,
|
||||||
|
} from "../common/string/filter/sequence-matching";
|
||||||
import {
|
import {
|
||||||
AreaRegistryEntry,
|
AreaRegistryEntry,
|
||||||
createAreaRegistryEntry,
|
createAreaRegistryEntry,
|
||||||
@ -28,6 +32,8 @@ import type { HaComboBox } from "./ha-combo-box";
|
|||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
|
type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry;
|
||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (
|
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (
|
||||||
item
|
item
|
||||||
) => html`<mwc-list-item
|
) => html`<mwc-list-item
|
||||||
@ -306,9 +312,12 @@ export class HaAreaPicker extends LitElement {
|
|||||||
this.entityFilter,
|
this.entityFilter,
|
||||||
this.noAdd,
|
this.noAdd,
|
||||||
this.excludeAreas
|
this.excludeAreas
|
||||||
);
|
).map((area) => ({
|
||||||
(this.comboBox as any).items = areas;
|
...area,
|
||||||
(this.comboBox as any).filteredItems = areas;
|
strings: [area.area_id, ...area.aliases, area.name],
|
||||||
|
}));
|
||||||
|
this.comboBox.items = areas;
|
||||||
|
this.comboBox.filteredItems = areas;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,8 +354,9 @@ export class HaAreaPicker extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredItems = this.comboBox.items?.filter((item) =>
|
const filteredItems = fuzzyFilterSort<ScorableAreaRegistryEntry>(
|
||||||
item.name.toLowerCase().includes(filter!.toLowerCase())
|
filter,
|
||||||
|
this.comboBox?.items || []
|
||||||
);
|
);
|
||||||
if (!this.noAdd && filteredItems?.length === 0) {
|
if (!this.noAdd && filteredItems?.length === 0) {
|
||||||
this._suggestion = filter;
|
this._suggestion = filter;
|
||||||
@ -409,7 +419,7 @@ export class HaAreaPicker extends LitElement {
|
|||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
const areas = [...Object.values(this.hass.areas), area];
|
const areas = [...Object.values(this.hass.areas), area];
|
||||||
(this.comboBox as any).filteredItems = this._getAreas(
|
this.comboBox.filteredItems = this._getAreas(
|
||||||
areas,
|
areas,
|
||||||
Object.values(this.hass.devices)!,
|
Object.values(this.hass.devices)!,
|
||||||
Object.values(this.hass.entities)!,
|
Object.values(this.hass.entities)!,
|
||||||
|
@ -62,6 +62,7 @@ class HaAttributes extends LitElement {
|
|||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
this.stateObj!,
|
this.stateObj!,
|
||||||
this.hass.locale,
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
this.hass.entities,
|
this.hass.entities,
|
||||||
attribute
|
attribute
|
||||||
)}
|
)}
|
||||||
|
@ -28,6 +28,7 @@ class HaClimateState extends LitElement {
|
|||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
this.stateObj,
|
this.stateObj,
|
||||||
this.hass.locale,
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
this.hass.entities,
|
this.hass.entities,
|
||||||
"preset_mode"
|
"preset_mode"
|
||||||
)}`
|
)}`
|
||||||
@ -136,6 +137,7 @@ class HaClimateState extends LitElement {
|
|||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
this.stateObj,
|
this.stateObj,
|
||||||
this.hass.locale,
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
this.hass.entities
|
this.hass.entities
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -144,6 +146,7 @@ class HaClimateState extends LitElement {
|
|||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
this.stateObj,
|
this.stateObj,
|
||||||
this.hass.locale,
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
this.hass.entities,
|
this.hass.entities,
|
||||||
"hvac_action"
|
"hvac_action"
|
||||||
)} (${stateString})`
|
)} (${stateString})`
|
||||||
|
618
src/components/ha-control-circular-slider.ts
Normal file
618
src/components/ha-control-circular-slider.ts
Normal file
@ -0,0 +1,618 @@
|
|||||||
|
import {
|
||||||
|
DIRECTION_ALL,
|
||||||
|
Manager,
|
||||||
|
Pan,
|
||||||
|
Tap,
|
||||||
|
TouchMouseInput,
|
||||||
|
} from "@egjs/hammerjs";
|
||||||
|
import {
|
||||||
|
CSSResultGroup,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
|
nothing,
|
||||||
|
svg,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { clamp } from "../common/number/clamp";
|
||||||
|
import { arc } from "../resources/svg-arc";
|
||||||
|
|
||||||
|
const MAX_ANGLE = 270;
|
||||||
|
const ROTATE_ANGLE = 360 - MAX_ANGLE / 2 - 90;
|
||||||
|
const RADIUS = 145;
|
||||||
|
|
||||||
|
function xy2polar(x: number, y: number) {
|
||||||
|
const r = Math.sqrt(x * x + y * y);
|
||||||
|
const phi = Math.atan2(y, x);
|
||||||
|
return [r, phi];
|
||||||
|
}
|
||||||
|
|
||||||
|
function rad2deg(rad: number) {
|
||||||
|
return (rad / (2 * Math.PI)) * 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveSlider = "low" | "high" | "value";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"value-changing": { value: unknown };
|
||||||
|
"low-changing": { value: unknown };
|
||||||
|
"low-changed": { value: unknown };
|
||||||
|
"high-changing": { value: unknown };
|
||||||
|
"high-changed": { value: unknown };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const A11Y_KEY_CODES = new Set([
|
||||||
|
"ArrowRight",
|
||||||
|
"ArrowUp",
|
||||||
|
"ArrowLeft",
|
||||||
|
"ArrowDown",
|
||||||
|
"PageUp",
|
||||||
|
"PageDown",
|
||||||
|
"Home",
|
||||||
|
"End",
|
||||||
|
]);
|
||||||
|
|
||||||
|
@customElement("ha-control-circular-slider")
|
||||||
|
export class HaControlCircularSlider extends LitElement {
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public dual?: boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
public inverted?: boolean;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public label?: string;
|
||||||
|
|
||||||
|
@property({ type: String, attribute: "low-label" })
|
||||||
|
public lowLabel?: string;
|
||||||
|
|
||||||
|
@property({ type: String, attribute: "high-label" })
|
||||||
|
public highLabel?: string;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public value?: number;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public low?: number;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public high?: number;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public current?: number;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public step = 1;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public min = 0;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public max = 100;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
public _localValue?: number = this.value;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
public _localLow?: number = this.low;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
public _localHigh?: number = this.high;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
public _activeSlider?: ActiveSlider;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
public _lastSlider?: ActiveSlider;
|
||||||
|
|
||||||
|
private _valueToPercentage(value: number) {
|
||||||
|
return (
|
||||||
|
(clamp(value, this.min, this.max) - this.min) / (this.max - this.min)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _percentageToValue(value: number) {
|
||||||
|
return (this.max - this.min) * value + this.min;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _steppedValue(value: number) {
|
||||||
|
return Math.round(value / this.step) * this.step;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _boundedValue(value: number) {
|
||||||
|
const min =
|
||||||
|
this._activeSlider === "high"
|
||||||
|
? Math.min(this._localLow ?? this.max)
|
||||||
|
: this.min;
|
||||||
|
const max =
|
||||||
|
this._activeSlider === "low"
|
||||||
|
? Math.max(this._localHigh ?? this.min)
|
||||||
|
: this.max;
|
||||||
|
return Math.min(Math.max(value, min), max);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps: PropertyValues): void {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
this._setupListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues): void {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (!this._activeSlider) {
|
||||||
|
if (changedProps.has("value")) {
|
||||||
|
this._localValue = this.value;
|
||||||
|
}
|
||||||
|
if (changedProps.has("low")) {
|
||||||
|
this._localLow = this.low;
|
||||||
|
}
|
||||||
|
if (changedProps.has("high")) {
|
||||||
|
this._localHigh = this.high;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._setupListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _mc?: HammerManager;
|
||||||
|
|
||||||
|
private _getPercentageFromEvent = (e: HammerInput) => {
|
||||||
|
const bound = this._slider.getBoundingClientRect();
|
||||||
|
const x = (2 * (e.center.x - bound.left - bound.width / 2)) / bound.width;
|
||||||
|
const y = (2 * (e.center.y - bound.top - bound.height / 2)) / bound.height;
|
||||||
|
|
||||||
|
const [, phi] = xy2polar(x, y);
|
||||||
|
|
||||||
|
const offset = (360 - MAX_ANGLE) / 2;
|
||||||
|
|
||||||
|
const angle = ((rad2deg(phi) + offset - ROTATE_ANGLE + 360) % 360) - offset;
|
||||||
|
|
||||||
|
return Math.max(Math.min(angle / MAX_ANGLE, 1), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
@query("#slider")
|
||||||
|
private _slider;
|
||||||
|
|
||||||
|
@query("#interaction")
|
||||||
|
private _interaction;
|
||||||
|
|
||||||
|
private _findActiveSlider(value: number): ActiveSlider {
|
||||||
|
if (!this.dual) return "value";
|
||||||
|
const low = Math.max(this._localLow ?? this.min, this.min);
|
||||||
|
const high = Math.min(this._localHigh ?? this.max, this.max);
|
||||||
|
if (low >= value) {
|
||||||
|
return "low";
|
||||||
|
}
|
||||||
|
if (high <= value) {
|
||||||
|
return "high";
|
||||||
|
}
|
||||||
|
const lowDistance = Math.abs(value - low);
|
||||||
|
const highDistance = Math.abs(value - high);
|
||||||
|
return lowDistance <= highDistance ? "low" : "high";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setActiveValue(value: number) {
|
||||||
|
switch (this._activeSlider) {
|
||||||
|
case "high":
|
||||||
|
this._localHigh = value;
|
||||||
|
break;
|
||||||
|
case "low":
|
||||||
|
this._localLow = value;
|
||||||
|
break;
|
||||||
|
case "value":
|
||||||
|
this._localValue = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getActiveValue(): number | undefined {
|
||||||
|
switch (this._activeSlider) {
|
||||||
|
case "high":
|
||||||
|
return this._localHigh;
|
||||||
|
case "low":
|
||||||
|
return this._localLow;
|
||||||
|
case "value":
|
||||||
|
return this._localValue;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupListeners() {
|
||||||
|
if (this._interaction && !this._mc) {
|
||||||
|
this._mc = new Manager(this._interaction, {
|
||||||
|
inputClass: TouchMouseInput,
|
||||||
|
});
|
||||||
|
this._mc.add(
|
||||||
|
new Pan({
|
||||||
|
direction: DIRECTION_ALL,
|
||||||
|
enable: true,
|
||||||
|
threshold: 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this._mc.add(new Tap({ event: "singletap" }));
|
||||||
|
|
||||||
|
this._mc.on("pan", (e) => {
|
||||||
|
e.srcEvent.stopPropagation();
|
||||||
|
e.srcEvent.preventDefault();
|
||||||
|
});
|
||||||
|
this._mc.on("panstart", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
const percentage = this._getPercentageFromEvent(e);
|
||||||
|
const raw = this._percentageToValue(percentage);
|
||||||
|
this._activeSlider = this._findActiveSlider(raw);
|
||||||
|
this._lastSlider = this._activeSlider;
|
||||||
|
this.shadowRoot?.getElementById("#slider")?.focus();
|
||||||
|
});
|
||||||
|
this._mc.on("pancancel", () => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this._activeSlider = undefined;
|
||||||
|
});
|
||||||
|
this._mc.on("panmove", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
const percentage = this._getPercentageFromEvent(e);
|
||||||
|
const raw = this._percentageToValue(percentage);
|
||||||
|
const bounded = this._boundedValue(raw);
|
||||||
|
this._setActiveValue(bounded);
|
||||||
|
const stepped = this._steppedValue(bounded);
|
||||||
|
if (this._activeSlider) {
|
||||||
|
fireEvent(this, `${this._activeSlider}-changing`, { value: stepped });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._mc.on("panend", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
const percentage = this._getPercentageFromEvent(e);
|
||||||
|
const raw = this._percentageToValue(percentage);
|
||||||
|
const bounded = this._boundedValue(raw);
|
||||||
|
const stepped = this._steppedValue(bounded);
|
||||||
|
this._setActiveValue(stepped);
|
||||||
|
if (this._activeSlider) {
|
||||||
|
fireEvent(this, `${this._activeSlider}-changing`, {
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
fireEvent(this, `${this._activeSlider}-changed`, { value: stepped });
|
||||||
|
}
|
||||||
|
this._activeSlider = undefined;
|
||||||
|
});
|
||||||
|
this._mc.on("singletap", (e) => {
|
||||||
|
if (this.disabled) return;
|
||||||
|
const percentage = this._getPercentageFromEvent(e);
|
||||||
|
const raw = this._percentageToValue(percentage);
|
||||||
|
this._activeSlider = this._findActiveSlider(raw);
|
||||||
|
const bounded = this._boundedValue(raw);
|
||||||
|
const stepped = this._steppedValue(bounded);
|
||||||
|
this._setActiveValue(stepped);
|
||||||
|
if (this._activeSlider) {
|
||||||
|
fireEvent(this, `${this._activeSlider}-changing`, {
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
fireEvent(this, `${this._activeSlider}-changed`, { value: stepped });
|
||||||
|
}
|
||||||
|
this._lastSlider = this._activeSlider;
|
||||||
|
this.shadowRoot?.getElementById("#slider")?.focus();
|
||||||
|
this._activeSlider = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _tenPercentStep() {
|
||||||
|
return Math.max(this.step, (this.max - this.min) / 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleKeyDown(e: KeyboardEvent) {
|
||||||
|
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (this._lastSlider) {
|
||||||
|
this.shadowRoot?.getElementById(this._lastSlider)?.focus();
|
||||||
|
}
|
||||||
|
this._activeSlider =
|
||||||
|
this._lastSlider ?? ((e.currentTarget as any).id as ActiveSlider);
|
||||||
|
this._lastSlider = undefined;
|
||||||
|
|
||||||
|
const value = this._getActiveValue();
|
||||||
|
|
||||||
|
switch (e.code) {
|
||||||
|
case "ArrowRight":
|
||||||
|
case "ArrowUp":
|
||||||
|
this._setActiveValue(
|
||||||
|
this._boundedValue((value ?? this.min) + this.step)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "ArrowLeft":
|
||||||
|
case "ArrowDown":
|
||||||
|
this._setActiveValue(
|
||||||
|
this._boundedValue((value ?? this.min) - this.step)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "PageUp":
|
||||||
|
this._setActiveValue(
|
||||||
|
this._steppedValue(
|
||||||
|
this._boundedValue((value ?? this.min) + this._tenPercentStep)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "PageDown":
|
||||||
|
this._setActiveValue(
|
||||||
|
this._steppedValue(
|
||||||
|
this._boundedValue((value ?? this.min) - this._tenPercentStep)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "Home":
|
||||||
|
this._setActiveValue(this._boundedValue(this.min));
|
||||||
|
break;
|
||||||
|
case "End":
|
||||||
|
this._setActiveValue(this._boundedValue(this.max));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
fireEvent(this, `${this._activeSlider}-changing`, {
|
||||||
|
value: this._getActiveValue(),
|
||||||
|
});
|
||||||
|
this._activeSlider = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleKeyUp(e: KeyboardEvent) {
|
||||||
|
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||||
|
this._activeSlider = (e.currentTarget as any).id as ActiveSlider;
|
||||||
|
e.preventDefault();
|
||||||
|
fireEvent(this, `${this._activeSlider}-changing`, {
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
fireEvent(this, `${this._activeSlider}-changed`, {
|
||||||
|
value: this._getActiveValue(),
|
||||||
|
});
|
||||||
|
this._activeSlider = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyListeners() {
|
||||||
|
if (this._mc) {
|
||||||
|
this._mc.destroy();
|
||||||
|
this._mc = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _strokeDashArc(
|
||||||
|
percentage: number,
|
||||||
|
inverted?: boolean
|
||||||
|
): [string, string] {
|
||||||
|
const maxRatio = MAX_ANGLE / 360;
|
||||||
|
const f = RADIUS * 2 * Math.PI;
|
||||||
|
if (inverted) {
|
||||||
|
const arcLength = (1 - percentage) * f * maxRatio;
|
||||||
|
const strokeDasharray = `${arcLength} ${f - arcLength}`;
|
||||||
|
const strokeDashOffset = `${arcLength + f * (1 - maxRatio)}`;
|
||||||
|
return [strokeDasharray, strokeDashOffset];
|
||||||
|
}
|
||||||
|
const arcLength = percentage * f * maxRatio;
|
||||||
|
const strokeDasharray = `${arcLength} ${f - arcLength}`;
|
||||||
|
const strokeDashOffset = "0";
|
||||||
|
return [strokeDasharray, strokeDashOffset];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const trackPath = arc({ x: 0, y: 0, start: 0, end: MAX_ANGLE, r: RADIUS });
|
||||||
|
|
||||||
|
const lowValue = this.dual ? this._localLow : this._localValue;
|
||||||
|
const highValue = this._localHigh;
|
||||||
|
const lowPercentage = this._valueToPercentage(lowValue ?? this.min);
|
||||||
|
const highPercentage = this._valueToPercentage(highValue ?? this.max);
|
||||||
|
|
||||||
|
const [lowStrokeDasharray, lowStrokeDashOffset] = this._strokeDashArc(
|
||||||
|
lowPercentage,
|
||||||
|
this.inverted
|
||||||
|
);
|
||||||
|
|
||||||
|
const [highStrokeDasharray, highStrokeDashOffset] = this._strokeDashArc(
|
||||||
|
highPercentage,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentPercentage = this._valueToPercentage(this.current ?? 0);
|
||||||
|
const currentAngle = currentPercentage * MAX_ANGLE;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<svg
|
||||||
|
id="slider"
|
||||||
|
viewBox="0 0 320 320"
|
||||||
|
overflow="visible"
|
||||||
|
class=${classMap({
|
||||||
|
pressed: Boolean(this._activeSlider),
|
||||||
|
})}
|
||||||
|
@keydown=${this._handleKeyDown}
|
||||||
|
tabindex=${this._lastSlider ? "0" : "-1"}
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
id="container"
|
||||||
|
transform="translate(160 160) rotate(${ROTATE_ANGLE})"
|
||||||
|
>
|
||||||
|
<g id="interaction">
|
||||||
|
<path d=${trackPath} />
|
||||||
|
</g>
|
||||||
|
<g id="display">
|
||||||
|
<path class="background" d=${trackPath} />
|
||||||
|
${lowValue != null
|
||||||
|
? svg`
|
||||||
|
<circle
|
||||||
|
.id=${this.dual ? "low" : "value"}
|
||||||
|
class="track"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r=${RADIUS}
|
||||||
|
stroke-dasharray=${lowStrokeDasharray}
|
||||||
|
stroke-dashoffset=${lowStrokeDashOffset}
|
||||||
|
role="slider"
|
||||||
|
tabindex="0"
|
||||||
|
aria-valuemin=${this.min}
|
||||||
|
aria-valuemax=${this.max}
|
||||||
|
aria-valuenow=${
|
||||||
|
lowValue != null ? this._steppedValue(lowValue) : undefined
|
||||||
|
}
|
||||||
|
aria-disabled=${this.disabled}
|
||||||
|
aria-label=${ifDefined(this.lowLabel ?? this.label)}
|
||||||
|
@keydown=${this._handleKeyDown}
|
||||||
|
@keyup=${this._handleKeyUp}
|
||||||
|
/>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${this.dual && highValue != null
|
||||||
|
? svg`
|
||||||
|
<circle
|
||||||
|
id="high"
|
||||||
|
class="track"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r=${RADIUS}
|
||||||
|
stroke-dasharray=${highStrokeDasharray}
|
||||||
|
stroke-dashoffset=${highStrokeDashOffset}
|
||||||
|
role="slider"
|
||||||
|
tabindex="0"
|
||||||
|
aria-valuemin=${this.min}
|
||||||
|
aria-valuemax=${this.max}
|
||||||
|
aria-valuenow=${
|
||||||
|
highValue != null
|
||||||
|
? this._steppedValue(highValue)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
aria-disabled=${this.disabled}
|
||||||
|
aria-label=${ifDefined(this.highLabel)}
|
||||||
|
@keydown=${this._handleKeyDown}
|
||||||
|
@keyup=${this._handleKeyUp}
|
||||||
|
/>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${this.current != null
|
||||||
|
? svg`
|
||||||
|
<g
|
||||||
|
style=${styleMap({ "--current-angle": `${currentAngle}deg` })}
|
||||||
|
class="current"
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
x1=${RADIUS - 12}
|
||||||
|
y1="0"
|
||||||
|
x2=${RADIUS - 15}
|
||||||
|
y2="0"
|
||||||
|
stroke-width="4"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1=${RADIUS - 15}
|
||||||
|
y1="0"
|
||||||
|
x2=${RADIUS - 20}
|
||||||
|
y2="0"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-width="4"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
--control-circular-slider-color: var(--primary-color);
|
||||||
|
--control-circular-slider-background: #8b97a3;
|
||||||
|
--control-circular-slider-background-opacity: 0.3;
|
||||||
|
--control-circular-slider-low-color: var(
|
||||||
|
--control-circular-slider-color
|
||||||
|
);
|
||||||
|
--control-circular-slider-high-color: var(
|
||||||
|
--control-circular-slider-color
|
||||||
|
);
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
width: 320px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#slider {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
#interaction {
|
||||||
|
display: flex;
|
||||||
|
fill: none;
|
||||||
|
stroke: transparent;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-width: 48px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#display {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
:host([disabled]) #interaction {
|
||||||
|
cursor: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--control-circular-slider-background);
|
||||||
|
opacity: var(--control-circular-slider-background-opacity);
|
||||||
|
transition: stroke 180ms ease-in-out, opacity 180ms ease-in-out;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track {
|
||||||
|
outline: none;
|
||||||
|
fill: none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-width: 24px;
|
||||||
|
transition: stroke-width 300ms ease-in-out,
|
||||||
|
stroke-dasharray 300ms ease-in-out,
|
||||||
|
stroke-dashoffset 300ms ease-in-out, stroke 180ms ease-in-out,
|
||||||
|
opacity 180ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track:focus-visible {
|
||||||
|
stroke-width: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pressed .track {
|
||||||
|
transition: stroke-width 300ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current {
|
||||||
|
stroke: var(--primary-text-color);
|
||||||
|
transform: rotate(var(--current-angle, 0));
|
||||||
|
transition: transform 300ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#value {
|
||||||
|
stroke: var(--control-circular-slider-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#low {
|
||||||
|
stroke: var(--control-circular-slider-low-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#high {
|
||||||
|
stroke: var(--control-circular-slider-high-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-control-circular-slider": HaControlCircularSlider;
|
||||||
|
}
|
||||||
|
}
|
@ -176,7 +176,7 @@ export class HaControlSlider extends LitElement {
|
|||||||
this._mc = undefined;
|
this._mc = undefined;
|
||||||
}
|
}
|
||||||
this.removeEventListener("keydown", this._handleKeyDown);
|
this.removeEventListener("keydown", this._handleKeyDown);
|
||||||
this.removeEventListener("keyup", this._handleKeyDown);
|
this.removeEventListener("keyup", this._handleKeyUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _tenPercentStep() {
|
private get _tenPercentStep() {
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { mdiCalendar } from "@mdi/js";
|
import { mdiCalendar } from "@mdi/js";
|
||||||
|
import { HassConfig } from "home-assistant-js-websocket";
|
||||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { formatDateNumeric } from "../common/datetime/format_date";
|
|
||||||
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
||||||
|
import { formatDateNumeric } from "../common/datetime/format_date";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { TimeZone } from "../data/translation";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
import "./ha-textfield";
|
import "./ha-textfield";
|
||||||
@ -59,7 +61,11 @@ export class HaDateInput extends LitElement {
|
|||||||
.value=${this.value
|
.value=${this.value
|
||||||
? formatDateNumeric(
|
? formatDateNumeric(
|
||||||
new Date(`${this.value.split("T")[0]}T00:00:00`),
|
new Date(`${this.value.split("T")[0]}T00:00:00`),
|
||||||
this.locale
|
{
|
||||||
|
...this.locale,
|
||||||
|
time_zone: TimeZone.local,
|
||||||
|
},
|
||||||
|
{} as HassConfig
|
||||||
)
|
)
|
||||||
: ""}
|
: ""}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
|
@ -3,6 +3,13 @@ import "@material/mwc-list/mwc-list";
|
|||||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import { mdiCalendar } from "@mdi/js";
|
import { mdiCalendar } from "@mdi/js";
|
||||||
|
import {
|
||||||
|
addDays,
|
||||||
|
endOfDay,
|
||||||
|
endOfWeek,
|
||||||
|
startOfDay,
|
||||||
|
startOfWeek,
|
||||||
|
} from "date-fns";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@ -12,10 +19,11 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { formatDateTime } from "../common/datetime/format_date_time";
|
import { calcDate } from "../common/datetime/calc_date";
|
||||||
import { formatDate } from "../common/datetime/format_date";
|
|
||||||
import { useAmPm } from "../common/datetime/use_am_pm";
|
|
||||||
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
||||||
|
import { formatDate } from "../common/datetime/format_date";
|
||||||
|
import { formatDateTime } from "../common/datetime/format_date_time";
|
||||||
|
import { useAmPm } from "../common/datetime/use_am_pm";
|
||||||
import { computeRTLDirection } from "../common/util/compute_rtl";
|
import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import "./date-range-picker";
|
import "./date-range-picker";
|
||||||
@ -34,7 +42,7 @@ export class HaDateRangePicker extends LitElement {
|
|||||||
|
|
||||||
@property() public endDate!: Date;
|
@property() public endDate!: Date;
|
||||||
|
|
||||||
@property() public ranges?: DateRangePickerRanges;
|
@property() public ranges?: DateRangePickerRanges | false;
|
||||||
|
|
||||||
@property() public autoApply = false;
|
@property() public autoApply = false;
|
||||||
|
|
||||||
@ -46,6 +54,70 @@ export class HaDateRangePicker extends LitElement {
|
|||||||
|
|
||||||
@property({ type: String }) private _rtlDirection = "ltr";
|
@property({ type: String }) private _rtlDirection = "ltr";
|
||||||
|
|
||||||
|
protected willUpdate() {
|
||||||
|
if (!this.hasUpdated && this.ranges === undefined) {
|
||||||
|
const today = new Date();
|
||||||
|
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
|
||||||
|
const weekStart = calcDate(
|
||||||
|
today,
|
||||||
|
startOfWeek,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
{
|
||||||
|
weekStartsOn,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const weekEnd = calcDate(
|
||||||
|
today,
|
||||||
|
endOfWeek,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
{
|
||||||
|
weekStartsOn,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.ranges = {
|
||||||
|
[this.hass.localize("ui.components.date-range-picker.ranges.today")]: [
|
||||||
|
calcDate(today, startOfDay, this.hass.locale, this.hass.config, {
|
||||||
|
weekStartsOn,
|
||||||
|
}),
|
||||||
|
calcDate(today, endOfDay, this.hass.locale, this.hass.config, {
|
||||||
|
weekStartsOn,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[this.hass.localize(
|
||||||
|
"ui.components.date-range-picker.ranges.yesterday"
|
||||||
|
)]: [
|
||||||
|
calcDate(
|
||||||
|
addDays(today, -1),
|
||||||
|
startOfDay,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
{
|
||||||
|
weekStartsOn,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
calcDate(
|
||||||
|
addDays(today, -1),
|
||||||
|
endOfDay,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
{
|
||||||
|
weekStartsOn,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[this.hass.localize(
|
||||||
|
"ui.components.date-range-picker.ranges.this_week"
|
||||||
|
)]: [weekStart, weekEnd],
|
||||||
|
[this.hass.localize(
|
||||||
|
"ui.components.date-range-picker.ranges.last_week"
|
||||||
|
)]: [addDays(weekStart, -7), addDays(weekEnd, -7)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
if (changedProps.has("hass")) {
|
if (changedProps.has("hass")) {
|
||||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
@ -65,15 +137,19 @@ export class HaDateRangePicker extends LitElement {
|
|||||||
twentyfour-hours=${this._hour24format}
|
twentyfour-hours=${this._hour24format}
|
||||||
start-date=${this.startDate}
|
start-date=${this.startDate}
|
||||||
end-date=${this.endDate}
|
end-date=${this.endDate}
|
||||||
?ranges=${this.ranges !== undefined}
|
?ranges=${this.ranges !== false}
|
||||||
first-day=${firstWeekdayIndex(this.hass.locale)}
|
first-day=${firstWeekdayIndex(this.hass.locale)}
|
||||||
>
|
>
|
||||||
<div slot="input" class="date-range-inputs">
|
<div slot="input" class="date-range-inputs">
|
||||||
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
.value=${this.timePicker
|
.value=${this.timePicker
|
||||||
? formatDateTime(this.startDate, this.hass.locale)
|
? formatDateTime(
|
||||||
: formatDate(this.startDate, this.hass.locale)}
|
this.startDate,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config
|
||||||
|
)
|
||||||
|
: formatDate(this.startDate, this.hass.locale, this.hass.config)}
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.components.date-range-picker.start_date"
|
"ui.components.date-range-picker.start_date"
|
||||||
)}
|
)}
|
||||||
@ -83,8 +159,8 @@ export class HaDateRangePicker extends LitElement {
|
|||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
.value=${this.timePicker
|
.value=${this.timePicker
|
||||||
? formatDateTime(this.endDate, this.hass.locale)
|
? formatDateTime(this.endDate, this.hass.locale, this.hass.config)
|
||||||
: formatDate(this.endDate, this.hass.locale)}
|
: formatDate(this.endDate, this.hass.locale, this.hass.config)}
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.components.date-range-picker.end_date"
|
"ui.components.date-range-picker.end_date"
|
||||||
)}
|
)}
|
||||||
|
@ -34,6 +34,8 @@ const getValue = (obj, item) =>
|
|||||||
|
|
||||||
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
||||||
|
|
||||||
|
const getWarning = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
||||||
|
|
||||||
@customElement("ha-form")
|
@customElement("ha-form")
|
||||||
export class HaForm extends LitElement implements HaFormElement {
|
export class HaForm extends LitElement implements HaFormElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@ -44,10 +46,14 @@ export class HaForm extends LitElement implements HaFormElement {
|
|||||||
|
|
||||||
@property() public error?: Record<string, string>;
|
@property() public error?: Record<string, string>;
|
||||||
|
|
||||||
|
@property() public warning?: Record<string, string>;
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@property() public computeError?: (schema: any, error) => string;
|
@property() public computeError?: (schema: any, error) => string;
|
||||||
|
|
||||||
|
@property() public computeWarning?: (schema: any, warning) => string;
|
||||||
|
|
||||||
@property() public computeLabel?: (
|
@property() public computeLabel?: (
|
||||||
schema: any,
|
schema: any,
|
||||||
data: HaFormDataContainer
|
data: HaFormDataContainer
|
||||||
@ -98,6 +104,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
|||||||
: ""}
|
: ""}
|
||||||
${this.schema.map((item) => {
|
${this.schema.map((item) => {
|
||||||
const error = getError(this.error, item);
|
const error = getError(this.error, item);
|
||||||
|
const warning = getWarning(this.warning, item);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${error
|
${error
|
||||||
@ -106,6 +113,12 @@ export class HaForm extends LitElement implements HaFormElement {
|
|||||||
${this._computeError(error, item)}
|
${this._computeError(error, item)}
|
||||||
</ha-alert>
|
</ha-alert>
|
||||||
`
|
`
|
||||||
|
: warning
|
||||||
|
? html`
|
||||||
|
<ha-alert own-margin alert-type="warning">
|
||||||
|
${this._computeWarning(warning, item)}
|
||||||
|
</ha-alert>
|
||||||
|
`
|
||||||
: ""}
|
: ""}
|
||||||
${"selector" in item
|
${"selector" in item
|
||||||
? html`<ha-selector
|
? html`<ha-selector
|
||||||
@ -187,6 +200,13 @@ export class HaForm extends LitElement implements HaFormElement {
|
|||||||
return this.computeError ? this.computeError(error, schema) : error;
|
return this.computeError ? this.computeError(error, schema) : error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _computeWarning(
|
||||||
|
warning,
|
||||||
|
schema: HaFormSchema | readonly HaFormSchema[]
|
||||||
|
) {
|
||||||
|
return this.computeWarning ? this.computeWarning(warning, schema) : warning;
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
.root > * {
|
.root > * {
|
||||||
|
@ -109,7 +109,8 @@ class HaHLSPlayer extends LitElement {
|
|||||||
private async _startHls(): Promise<void> {
|
private async _startHls(): Promise<void> {
|
||||||
const masterPlaylistPromise = fetch(this.url);
|
const masterPlaylistPromise = fetch(this.url);
|
||||||
|
|
||||||
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light")).default;
|
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.mjs"))
|
||||||
|
.default;
|
||||||
|
|
||||||
if (!this.isConnected) {
|
if (!this.isConnected) {
|
||||||
return;
|
return;
|
||||||
|
@ -186,9 +186,8 @@ class HaHsColorPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
if (changedProps.has("value")) {
|
if (changedProps.has("value")) {
|
||||||
if (
|
if (
|
||||||
this.value !== undefined &&
|
this._localValue?.[0] !== this.value?.[0] ||
|
||||||
(this._localValue?.[0] !== this.value[0] ||
|
this._localValue?.[1] !== this.value?.[1]
|
||||||
this._localValue?.[1] !== this.value[1])
|
|
||||||
) {
|
) {
|
||||||
this._resetPosition();
|
this._resetPosition();
|
||||||
}
|
}
|
||||||
@ -243,7 +242,11 @@ class HaHsColorPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _resetPosition() {
|
private _resetPosition() {
|
||||||
if (this.value === undefined) return;
|
if (this.value === undefined) {
|
||||||
|
this._cursorPosition = undefined;
|
||||||
|
this._localValue = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._cursorPosition = this._getCoordsFromValue(this.value);
|
this._cursorPosition = this._getCoordsFromValue(this.value);
|
||||||
this._localValue = this.value;
|
this._localValue = this.value;
|
||||||
}
|
}
|
||||||
@ -373,17 +376,20 @@ class HaHsColorPicker extends LitElement {
|
|||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
canvas {
|
canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
svg {
|
svg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
137
src/components/ha-humidifier-state.ts
Normal file
137
src/components/ha-humidifier-state.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
|
||||||
|
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||||
|
import { formatNumber } from "../common/number/format_number";
|
||||||
|
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||||
|
import { isUnavailableState, OFF } from "../data/entity";
|
||||||
|
import { HumidifierEntity } from "../data/humidifier";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
@customElement("ha-humidifier-state")
|
||||||
|
class HaHumidifierState extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public stateObj!: HumidifierEntity;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const currentStatus = this._computeCurrentStatus();
|
||||||
|
|
||||||
|
return html`<div class="target">
|
||||||
|
${!isUnavailableState(this.stateObj.state)
|
||||||
|
? html`<span class="state-label">
|
||||||
|
${this._localizeState()}
|
||||||
|
${this.stateObj.attributes.mode
|
||||||
|
? html`-
|
||||||
|
${computeAttributeValueDisplay(
|
||||||
|
this.hass.localize,
|
||||||
|
this.stateObj,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
this.hass.entities,
|
||||||
|
"mode"
|
||||||
|
)}`
|
||||||
|
: ""}
|
||||||
|
</span>
|
||||||
|
<div class="unit">${this._computeTarget()}</div>`
|
||||||
|
: this._localizeState()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${currentStatus && !isUnavailableState(this.stateObj.state)
|
||||||
|
? html`<div class="current">
|
||||||
|
${this.hass.localize("ui.card.climate.currently")}:
|
||||||
|
<div class="unit">${currentStatus}</div>
|
||||||
|
</div>`
|
||||||
|
: ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeCurrentStatus(): string | undefined {
|
||||||
|
if (!this.hass || !this.stateObj) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.stateObj.attributes.current_humidity != null) {
|
||||||
|
return `${formatNumber(
|
||||||
|
this.stateObj.attributes.current_humidity,
|
||||||
|
this.hass.locale
|
||||||
|
)}${blankBeforePercent(this.hass.locale)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeTarget(): string {
|
||||||
|
if (!this.hass || !this.stateObj) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.stateObj.attributes.humidity != null) {
|
||||||
|
return `${formatNumber(
|
||||||
|
this.stateObj.attributes.humidity,
|
||||||
|
this.hass.locale
|
||||||
|
)}${blankBeforePercent(this.hass.locale)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _localizeState(): string {
|
||||||
|
if (isUnavailableState(this.stateObj.state)) {
|
||||||
|
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateString = computeStateDisplay(
|
||||||
|
this.hass.localize,
|
||||||
|
this.stateObj,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
this.hass.entities
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.stateObj.attributes.action && this.stateObj.state !== OFF
|
||||||
|
? `${computeAttributeValueDisplay(
|
||||||
|
this.hass.localize,
|
||||||
|
this.stateObj,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
this.hass.entities,
|
||||||
|
"action"
|
||||||
|
)} (${stateString})`
|
||||||
|
: stateString;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.current {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-label {
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
display: inline-block;
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-humidifier-state": HaHumidifierState;
|
||||||
|
}
|
||||||
|
}
|
38
src/components/ha-icon-button-group.ts
Normal file
38
src/components/ha-icon-button-group.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
|
||||||
|
@customElement("ha-icon-button-group")
|
||||||
|
export class HaIconButtonGroup extends LitElement {
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`<slot></slot>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 28px;
|
||||||
|
background-color: rgba(139, 145, 151, 0.1);
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: auto;
|
||||||
|
padding: 4px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
::slotted(.separator) {
|
||||||
|
background-color: rgba(var(--rgb-primary-text-color), 0.15);
|
||||||
|
width: 1px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-icon-button-group": HaIconButtonGroup;
|
||||||
|
}
|
||||||
|
}
|
52
src/components/ha-icon-button-toggle.ts
Normal file
52
src/components/ha-icon-button-toggle.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { css, CSSResultGroup } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { HaIconButton } from "./ha-icon-button";
|
||||||
|
|
||||||
|
@customElement("ha-icon-button-toggle")
|
||||||
|
export class HaIconButtonToggle extends HaIconButton {
|
||||||
|
@property({ type: Boolean, reflect: true }) selected = false;
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
mwc-icon-button {
|
||||||
|
position: relative;
|
||||||
|
transition: color 180ms ease-in-out;
|
||||||
|
}
|
||||||
|
mwc-icon-button::before {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 180ms ease-in-out;
|
||||||
|
background-color: var(--primary-text-color);
|
||||||
|
border-radius: 20px;
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
left: -10px;
|
||||||
|
bottom: -10px;
|
||||||
|
right: -10px;
|
||||||
|
margin: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
:host([border-only]) mwc-icon-button::before {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 2px solid var(--primary-text-color);
|
||||||
|
}
|
||||||
|
:host([selected]) mwc-icon-button {
|
||||||
|
color: var(--primary-background-color);
|
||||||
|
}
|
||||||
|
:host([selected]) mwc-icon-button::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-icon-button-toggle": HaIconButtonToggle;
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,8 @@ import { customElement, property } from "lit/decorators";
|
|||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { renderMarkdown } from "../resources/render-markdown";
|
import { renderMarkdown } from "../resources/render-markdown";
|
||||||
|
|
||||||
|
const _blockQuoteToAlert = { Note: "info", Warning: "warning" };
|
||||||
|
|
||||||
@customElement("ha-markdown-element")
|
@customElement("ha-markdown-element")
|
||||||
class HaMarkdownElement extends ReactiveElement {
|
class HaMarkdownElement extends ReactiveElement {
|
||||||
@property() public content?;
|
@property() public content?;
|
||||||
@ -65,6 +67,34 @@ class HaMarkdownElement extends ReactiveElement {
|
|||||||
node.loading = "lazy";
|
node.loading = "lazy";
|
||||||
}
|
}
|
||||||
node.addEventListener("load", this._resize);
|
node.addEventListener("load", this._resize);
|
||||||
|
} else if (node instanceof HTMLQuoteElement) {
|
||||||
|
// Map GitHub blockquote elements to our ha-alert element
|
||||||
|
const firstElementChild = node.firstElementChild;
|
||||||
|
const quoteTitleElement = firstElementChild?.firstElementChild;
|
||||||
|
const quoteType =
|
||||||
|
quoteTitleElement?.textContent &&
|
||||||
|
_blockQuoteToAlert[quoteTitleElement.textContent];
|
||||||
|
|
||||||
|
// GitHub is strict on how these are defined, we need to make sure we know what we have before starting to replace it
|
||||||
|
if (quoteTitleElement?.nodeName === "STRONG" && quoteType) {
|
||||||
|
const alertNote = document.createElement("ha-alert");
|
||||||
|
alertNote.alertType = quoteType;
|
||||||
|
alertNote.title =
|
||||||
|
(firstElementChild!.childNodes[1].nodeName === "#text" &&
|
||||||
|
firstElementChild!.childNodes[1].textContent?.trimStart()) ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
const childNodes = Array.from(firstElementChild!.childNodes);
|
||||||
|
for (const child of childNodes.slice(
|
||||||
|
childNodes.findIndex(
|
||||||
|
// There is always a line break between the title and the content, we want to skip that
|
||||||
|
(childNode) => childNode instanceof HTMLBRElement
|
||||||
|
) + 1
|
||||||
|
)) {
|
||||||
|
alertNote.appendChild(child);
|
||||||
|
}
|
||||||
|
node.firstElementChild!.replaceWith(alertNote);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,20 +73,25 @@ class HaMenuButton extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
const oldHass = changedProps.has("hass")
|
||||||
const oldNarrow =
|
? (changedProps.get("hass") as HomeAssistant | undefined)
|
||||||
changedProps.get("narrow") ||
|
: this.hass;
|
||||||
(oldHass && oldHass.dockedSidebar === "always_hidden");
|
const oldNarrow = changedProps.has("narrow")
|
||||||
const newNarrow =
|
? (changedProps.get("narrow") as boolean | undefined)
|
||||||
|
: this.narrow;
|
||||||
|
|
||||||
|
const oldShowButton =
|
||||||
|
oldNarrow || oldHass?.dockedSidebar === "always_hidden";
|
||||||
|
const showButton =
|
||||||
this.narrow || this.hass.dockedSidebar === "always_hidden";
|
this.narrow || this.hass.dockedSidebar === "always_hidden";
|
||||||
|
|
||||||
if (oldNarrow === newNarrow) {
|
if (oldShowButton === showButton) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.style.display = newNarrow || this._alwaysVisible ? "initial" : "none";
|
this.style.display = showButton || this._alwaysVisible ? "initial" : "none";
|
||||||
|
|
||||||
if (!newNarrow) {
|
if (!showButton) {
|
||||||
if (this._unsubNotifications) {
|
if (this._unsubNotifications) {
|
||||||
this._unsubNotifications();
|
this._unsubNotifications();
|
||||||
this._unsubNotifications = undefined;
|
this._unsubNotifications = undefined;
|
||||||
@ -98,6 +103,9 @@ class HaMenuButton extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _subscribeNotifications() {
|
private _subscribeNotifications() {
|
||||||
|
if (this._unsubNotifications) {
|
||||||
|
throw new Error("Already subscribed");
|
||||||
|
}
|
||||||
this._unsubNotifications = subscribeNotifications(
|
this._unsubNotifications = subscribeNotifications(
|
||||||
this.hass.connection,
|
this.hass.connection,
|
||||||
(notifications) => {
|
(notifications) => {
|
||||||
|
@ -2,7 +2,7 @@ import { mdiImagePlus } from "@mdi/js";
|
|||||||
import { html, LitElement, TemplateResult } from "lit";
|
import { 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 { createImage, generateImageThumbnailUrl } from "../data/image";
|
import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
|
||||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||||
import {
|
import {
|
||||||
CropOptions,
|
CropOptions,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user