20230628.0 (#17080)

This commit is contained in:
Bram Kragten 2023-06-28 17:30:13 +02:00 committed by GitHub
commit 50f4a1abc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
362 changed files with 11124 additions and 4504 deletions

View File

@ -10,6 +10,12 @@ supports es6-module-dynamic-import
not Safari < 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
not dead

View File

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

View File

@ -1,3 +1,8 @@
categories:
- title: 'Dependency updates'
collapse-after: 3
labels:
- 'dependencies'
template: |
## What's Changed

View File

@ -21,7 +21,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
with:
ref: dev
@ -57,7 +57,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
with:
ref: master

View File

@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.6.0
with:
@ -47,7 +47,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.6.0
with:
@ -65,7 +65,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.6.0
with:
@ -83,7 +83,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.6.0
with:

View File

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

View File

@ -22,7 +22,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
with:
ref: dev
@ -58,7 +58,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
with:
ref: master

View File

@ -16,7 +16,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.6.0

View File

@ -21,7 +21,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Setup Node
uses: actions/setup-node@v3.6.0

View File

@ -9,7 +9,7 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v4.0.0
- uses: dessant/lock-threads@v4.0.1
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: "30"

View File

@ -6,7 +6,7 @@ on:
- cron: "0 1 * * *"
env:
PYTHON_VERSION: "3.10"
PYTHON_VERSION: "3.11"
NODE_OPTIONS: --max_old_space_size=6144
permissions:
@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4

View File

@ -5,8 +5,17 @@ on:
branches:
- dev
permissions:
contents: read
jobs:
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
steps:
- uses: release-drafter/release-drafter@v5

View File

@ -6,7 +6,7 @@ on:
- published
env:
PYTHON_VERSION: "3.10"
PYTHON_VERSION: "3.11"
NODE_OPTIONS: --max_old_space_size=6144
# Set default workflow permissions
@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
@ -76,7 +76,7 @@ jobs:
- name: Build wheels
uses: home-assistant/wheels@2023.04.0
with:
abi: cp310
abi: cp311
tag: musllinux_1_2
arch: amd64
wheels-key: ${{ secrets.WHEELS_KEY }}

View File

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

View File

@ -77,6 +77,7 @@ module.exports.htmlMinifierOptions = {
module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
safari10: !latestBuild,
ecma: latestBuild ? 2015 : 5,
module: latestBuild,
format: { comments: false },
sourceMap: !isTestBuild,
});
@ -97,7 +98,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
"@babel/preset-env",
{
useBuiltIns: latestBuild ? false : "entry",
corejs: latestBuild ? false : { version: "3.30", proposals: true },
corejs: latestBuild ? false : { version: "3.31", proposals: true },
bugfixes: true,
},
],

View File

@ -17,6 +17,7 @@ const modules = {
"intl-datetimeformat": "DateTimeFormat",
"intl-numberformat": "NumberFormat",
"intl-displaynames": "DisplayNames",
"intl-listformat": "ListFormat",
};
gulp.task("create-locale-data", (done) => {

View File

@ -142,4 +142,5 @@ module.exports = {
createCastConfig,
createHassioConfig,
createGalleryConfig,
createRollupConfig,
};

View File

@ -41,7 +41,7 @@ const createWebpackConfig = ({
return {
name,
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 production, generate source maps for accurate stack traces without source code
// For development, generate "cheap" versions that can map to original line numbers
@ -84,6 +84,13 @@ const createWebpackConfig = ({
],
moduleIds: 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: [
!isStatsBuild && new WebpackBar({ fancy: !isProdBuild }),
@ -160,9 +167,12 @@ const createWebpackConfig = ({
"lit/polyfill-support$": "lit/polyfill-support.js",
"@lit-labs/virtualizer/layouts/grid":
"@lit-labs/virtualizer/layouts/grid.js",
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver":
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
},
},
output: {
module: latestBuild,
filename: ({ chunk }) =>
!isProdBuild || isStatsBuild || dontHash.has(chunk.name)
? "[name].js"
@ -196,7 +206,7 @@ const createWebpackConfig = ({
: undefined,
},
experiments: {
topLevelAwait: true,
outputModule: true,
},
};
};
@ -243,4 +253,5 @@ module.exports = {
createCastConfig,
createHassioConfig,
createGalleryConfig,
createWebpackConfig,
};

View 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",
},
];

View File

@ -41,6 +41,7 @@ const triggers = [
{ platform: "sun", event: "sunset" },
{ platform: "time_pattern" },
{ platform: "webhook" },
{ platform: "persistent_notification" },
{
platform: "zone",
entity_id: "person.person",
@ -50,6 +51,11 @@ const triggers = [
{ platform: "tag" },
{ platform: "time", at: "15:32" },
{ 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" },
];

View File

@ -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 { 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 { 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 { 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 { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
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[] }[] = [
{
@ -72,6 +74,16 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
triggers: [{ platform: "webhook", ...HaWebhookTrigger.defaultConfig }],
},
{
name: "Persistent Notification",
triggers: [
{
platform: "persistent_notification",
...HaPersistentNotificationTrigger.defaultConfig,
},
],
},
{
name: "Zone",
triggers: [{ platform: "zone", ...HaZoneTrigger.defaultConfig }],
@ -101,6 +113,16 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
name: "Device Trigger",
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")

View File

@ -0,0 +1,3 @@
---
title: Control Circular Slider
---

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

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

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

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

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

View File

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

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

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

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

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

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

View File

@ -1,5 +1,5 @@
---
title: (Numeric) Date Formatting
title: Date Format (Numeric)
---
This pages lists all supported languages with their available (numeric) date formats.

View File

@ -1,27 +1,28 @@
import { html, css, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-card";
import { HomeAssistant } from "../../../../src/types";
import { translationMetadata } from "../../../../src/resources/translations-metadata";
import "@material/mwc-list/mwc-list";
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import { formatDateNumeric } from "../../../../src/common/datetime/format_date";
import "../../../../src/components/ha-card";
import {
DateFormat,
FirstWeekday,
FrontendLocaleData,
NumberFormat,
TimeFormat,
DateFormat,
FirstWeekday,
TimeZone,
} from "../../../../src/data/translation";
import { demoConfig } from "../../../../src/fake_data/demo_config";
import { translationMetadata } from "../../../../src/resources/translations-metadata";
@customElement("demo-date-time-date")
export class DemoDateTimeDate extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
protected render() {
const defaultLocale: FrontendLocaleData = {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.language,
date_format: DateFormat.language,
time_zone: TimeZone.local,
first_weekday: FirstWeekday.language,
};
const date = new Date();
@ -41,32 +42,48 @@ export class DemoDateTimeDate extends LitElement {
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatDateNumeric(date, {
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.language,
})}
},
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(date, {
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.DMY,
})}
},
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(date, {
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.MDY,
})}
},
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(date, {
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.YMD,
})}
},
demoConfig
)}
</div>
</div>
`

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

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

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

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

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

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

View File

@ -9,7 +9,7 @@ const CONFIGS = [
heading: "markdown-it demo",
config: `
- type: markdown
content: >-
content: |
# h1 Heading 8-)
## h2 Heading
@ -65,6 +65,15 @@ const CONFIGS = [
>> ...by using additional greater-than signs right next to each other...
> > > ...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

View File

@ -135,6 +135,9 @@ const ENTITIES: HassEntity[] = [
createEntity("climate.fan_only", "fan_only"),
createEntity("climate.auto_idle", "auto", undefined, { hvac_action: "idle" }),
createEntity("climate.auto_off", "auto", undefined, { hvac_action: "off" }),
createEntity("climate.auto_preheating", "auto", undefined, {
hvac_action: "preheating",
}),
createEntity("climate.auto_heating", "auto", undefined, {
hvac_action: "heating",
}),
@ -354,6 +357,7 @@ export class DemoEntityState extends LitElement {
hass.localize,
entry.stateObj,
hass.locale,
hass.config,
hass.entities
)}`,
},

View File

@ -114,11 +114,22 @@ class HassioAddonInfo extends LitElement {
@state() private _error?: string;
private _fetchDataTimeout?: number;
private _addonStoreInfo = memoizeOne(
(slug: string, storeAddons: StoreAddon[]) =>
storeAddons.find((addon) => addon.slug === slug)
);
public disconnectedCallback() {
super.disconnectedCallback();
if (this._fetchDataTimeout) {
clearInterval(this._fetchDataTimeout);
this._fetchDataTimeout = undefined;
}
}
protected render(): TemplateResult {
const addonStoreInfo =
!this.addon.detached && !this.addon.available
@ -592,7 +603,10 @@ class HassioAddonInfo extends LitElement {
</ha-progress-button>
`
: html`
<ha-progress-button @click=${this._startClicked}>
<ha-progress-button
@click=${this._startClicked}
.progress=${this.addon.state === "startup"}
>
${this.supervisor.localize("addon.dashboard.start")}
</ha-progress-button>
`
@ -672,8 +686,35 @@ class HassioAddonInfo extends LitElement {
super.updated(changedProps);
if (changedProps.has("addon")) {
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> {
if ("state" in this.addon && this.addon.state === "started") {

View File

@ -136,6 +136,15 @@ export class HassioBackups extends LitElement {
sortable: true,
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: {
title: this.supervisor.localize("backup.created"),
width: "15%",

View File

@ -143,7 +143,11 @@ export class SupervisorBackupContent extends LitElement {
: this._localize("partial_backup")}
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
${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}
</div>`
: html`<paper-input
@ -336,7 +340,9 @@ export class SupervisorBackupContent extends LitElement {
const data: any = {};
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) {

View File

@ -137,6 +137,9 @@ class HassioAddons extends LitElement {
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
}
.content {
margin-bottom: 72px;
}
`,
];
}

View File

@ -16,6 +16,7 @@ import "../../../src/components/ha-icon-button";
import {
fetchHassioAddonInfo,
HassioAddonDetails,
startHassioAddon,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import {
@ -23,7 +24,10 @@ import {
validateHassioSession,
} from "../../../src/data/hassio/ingress";
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-subpage";
import { HomeAssistant, Route } from "../../../src/types";
@ -45,6 +49,8 @@ class HassioIngressView extends LitElement {
private _sessionKeepAlive?: number;
private _fetchDataTimeout?: number;
public disconnectedCallback() {
super.disconnectedCallback();
@ -52,6 +58,10 @@ class HassioIngressView extends LitElement {
clearInterval(this._sessionKeepAlive);
this._sessionKeepAlive = undefined;
}
if (this._fetchDataTimeout) {
clearInterval(this._fetchDataTimeout);
this._fetchDataTimeout = undefined;
}
}
protected render(): TemplateResult {
@ -62,6 +72,7 @@ class HassioIngressView extends LitElement {
const iframe = html`<iframe
title=${this._addon.name}
src=${this._addon.ingress_url!}
@load=${this._checkLoaded}
>
</iframe>`;
@ -132,10 +143,10 @@ class HassioIngressView extends LitElement {
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 oldAddon = oldRoute ? oldRoute.path.substr(1) : undefined;
const oldAddon = oldRoute ? oldRoute.path.substring(1) : undefined;
if (addon && addon !== oldAddon) {
this._fetchData(addon);
@ -145,33 +156,23 @@ class HassioIngressView extends LitElement {
private async _fetchData(addonSlug: string) {
const createSessionPromise = createHassioSession(this.hass);
let addon;
let addon: HassioAddonDetails;
try {
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
} catch (err: any) {
await showAlertDialog(this, {
text: "Unable to fetch add-on info to start Ingress",
text: this.supervisor.localize("ingress.error_addon_info"),
title: "Supervisor",
});
await nextRender();
history.back();
navigate("/hassio/store", { replace: true });
return;
}
if (!addon.ingress_url) {
if (!addon.version) {
await showAlertDialog(this, {
text: "Add-on does not support Ingress",
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",
text: this.supervisor.localize("ingress.error_addon_not_installed"),
title: addon.name,
});
await nextRender();
@ -179,13 +180,74 @@ class HassioIngressView extends LitElement {
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 {
session = await createSessionPromise;
} catch (err: any) {
if (this._sessionKeepAlive) {
clearInterval(this._sessionKeepAlive);
}
await showAlertDialog(this, {
text: "Unable to create an Ingress session",
text: this.supervisor.localize("ingress.error_creating_session"),
title: addon.name,
});
await nextRender();
@ -207,6 +269,31 @@ class HassioIngressView extends LitElement {
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 {
fireEvent(this, "hass-toggle-menu");
}

View File

@ -25,33 +25,34 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.22.3",
"@babel/runtime": "7.22.5",
"@braintree/sanitize-url": "6.0.2",
"@codemirror/autocomplete": "6.7.1",
"@codemirror/autocomplete": "6.8.1",
"@codemirror/commands": "6.2.4",
"@codemirror/language": "6.7.0",
"@codemirror/language": "6.8.0",
"@codemirror/legacy-modes": "6.3.2",
"@codemirror/search": "6.4.0",
"@codemirror/search": "6.5.0",
"@codemirror/state": "6.2.1",
"@codemirror/view": "6.12.0",
"@codemirror/view": "6.14.0",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.8.0",
"@formatjs/intl-displaynames": "6.3.2",
"@formatjs/intl-getcanonicallocales": "2.2.0",
"@formatjs/intl-locale": "3.3.0",
"@formatjs/intl-numberformat": "8.5.0",
"@formatjs/intl-pluralrules": "5.2.2",
"@formatjs/intl-relativetimeformat": "11.2.2",
"@formatjs/intl-datetimeformat": "6.10.0",
"@formatjs/intl-displaynames": "6.5.0",
"@formatjs/intl-getcanonicallocales": "2.2.1",
"@formatjs/intl-listformat": "7.4.0",
"@formatjs/intl-locale": "3.3.2",
"@formatjs/intl-numberformat": "8.7.0",
"@formatjs/intl-pluralrules": "5.2.4",
"@formatjs/intl-relativetimeformat": "11.2.4",
"@fullcalendar/core": "6.1.8",
"@fullcalendar/daygrid": "6.1.8",
"@fullcalendar/interaction": "6.1.8",
"@fullcalendar/list": "6.1.8",
"@fullcalendar/timegrid": "6.1.8",
"@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/virtualizer": "2.0.2",
"@lrnwebcomponents/simple-tooltip": "7.0.2",
"@lit-labs/virtualizer": "2.0.3",
"@lrnwebcomponents/simple-tooltip": "7.0.5",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-button": "0.27.0",
@ -77,7 +78,7 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "=1.0.0-pre.9",
"@material/web": "=1.0.0-pre.11",
"@mdi/js": "7.2.96",
"@mdi/svg": "7.2.96",
"@polymer/app-layout": "3.1.0",
@ -92,8 +93,8 @@
"@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.0.8",
"@vaadin/vaadin-themable-mixin": "24.0.8",
"@vaadin/combo-box": "24.1.1",
"@vaadin/vaadin-themable-mixin": "24.1.1",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@ -103,7 +104,7 @@
"app-datepicker": "5.1.1",
"chart.js": "3.3.2",
"comlink": "4.4.1",
"core-js": "3.30.2",
"core-js": "3.31.0",
"cropperjs": "1.5.13",
"date-fns": "2.30.0",
"date-fns-tz": "2.0.0",
@ -111,10 +112,10 @@
"deep-freeze": "0.0.1",
"fuse.js": "6.6.2",
"google-timezones-json": "1.1.0",
"hls.js": "1.4.4",
"home-assistant-js-websocket": "8.0.1",
"hls.js": "1.4.6",
"home-assistant-js-websocket": "8.1.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.3.5",
"intl-messageformat": "10.5.0",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "1.0.4",
@ -131,9 +132,9 @@
"rrule": "2.7.2",
"sortablejs": "1.15.0",
"superstruct": "1.0.3",
"tinykeys": "1.4.0",
"tsparticles-engine": "2.9.3",
"tsparticles-preset-links": "2.9.3",
"tinykeys": "2.1.0",
"tsparticles-engine": "2.10.1",
"tsparticles-preset-links": "2.10.1",
"unfetch": "5.0.0",
"vis-data": "7.1.6",
"vis-network": "9.1.6",
@ -149,18 +150,18 @@
"xss": "1.0.14"
},
"devDependencies": {
"@babel/core": "7.22.1",
"@babel/plugin-proposal-decorators": "7.22.3",
"@babel/plugin-transform-runtime": "7.22.4",
"@babel/preset-env": "7.22.4",
"@babel/preset-typescript": "7.21.5",
"@babel/core": "7.22.5",
"@babel/plugin-proposal-decorators": "7.22.5",
"@babel/plugin-transform-runtime": "7.22.5",
"@babel/preset-env": "7.22.5",
"@babel/preset-typescript": "7.22.5",
"@koa/cors": "4.0.0",
"@octokit/auth-oauth-device": "4.0.4",
"@octokit/plugin-retry": "5.0.0",
"@octokit/rest": "19.0.11",
"@octokit/auth-oauth-device": "5.0.2",
"@octokit/plugin-retry": "5.0.4",
"@octokit/rest": "19.0.13",
"@open-wc/dev-server-hmr": "0.1.4",
"@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-node-resolve": "15.1.0",
"@rollup/plugin-replace": "5.0.2",
@ -172,7 +173,7 @@
"@types/html-minifier-terser": "7.0.0",
"@types/js-yaml": "4.0.5",
"@types/leaflet": "1.9.3",
"@types/leaflet-draw": "1.0.6",
"@types/leaflet-draw": "1.0.7",
"@types/marked": "4.3.1",
"@types/mocha": "10.0.1",
"@types/qrcode": "1.5.0",
@ -180,15 +181,15 @@
"@types/sortablejs": "1.15.1",
"@types/tar": "6.1.5",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "5.59.8",
"@typescript-eslint/parser": "5.59.8",
"@typescript-eslint/eslint-plugin": "5.60.0",
"@typescript-eslint/parser": "5.60.0",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.2",
"babel-plugin-template-html-minifier": "4.1.0",
"chai": "4.3.7",
"del": "7.0.0",
"eslint": "8.42.0",
"eslint": "8.43.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.0.0",
"eslint-config-prettier": "8.8.0",
@ -196,13 +197,13 @@
"eslint-plugin-disable": "2.0.3",
"eslint-plugin-import": "2.27.5",
"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-wc": "1.5.0",
"esprima": "4.0.1",
"fancy-log": "2.0.0",
"fs-extra": "11.1.1",
"glob": "10.2.6",
"glob": "10.3.0",
"gulp": "4.0.2",
"gulp-flatmap": "1.0.2",
"gulp-json-transform": "0.4.8",
@ -214,7 +215,7 @@
"instant-mocha": "1.5.1",
"jszip": "3.10.1",
"lint-staged": "13.2.2",
"lit-analyzer": "1.2.1",
"lit-analyzer": "2.0.0-pre.3",
"lodash.template": "4.5.0",
"magic-string": "0.30.0",
"map-stream": "0.0.7",
@ -227,20 +228,20 @@
"rollup": "2.79.1",
"rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.9.0",
"rollup-plugin-visualizer": "5.9.2",
"serve-handler": "6.1.5",
"sinon": "15.1.0",
"sinon": "15.2.0",
"source-map-url": "0.4.1",
"systemjs": "6.14.1",
"tar": "6.1.15",
"terser-webpack-plugin": "5.3.9",
"ts-lit-plugin": "1.2.1",
"typescript": "4.9.5",
"ts-lit-plugin": "2.0.0-pre.1",
"typescript": "5.1.3",
"vinyl-buffer": "1.0.1",
"vinyl-source-stream": "2.0.0",
"webpack": "5.84.1",
"webpack-cli": "5.1.1",
"webpack-dev-server": "4.15.0",
"webpack": "5.88.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1",
"webpack-manifest-plugin": "5.0.0",
"webpackbar": "5.0.2",
"workbox-build": "7.0.0"

View File

@ -1,17 +1,17 @@
[build-system]
requires = ["setuptools~=62.3", "wheel~=0.37.1"]
requires = ["setuptools~=68.0", "wheel~=0.40.0"]
build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20230608.0"
version = "20230628.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
authors = [
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
]
requires-python = ">=3.4.0"
requires-python = ">=3.10.0"
[project.urls]
"Homepage" = "https://github.com/home-assistant/frontend"

View File

@ -19,14 +19,20 @@
},
"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"],
"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"],
"allowedVersions": "< 3"
},
{
"description": "Group tsparticles engine and presets",
"groupName": "tsparticles",
"matchPackageNames": ["tsparticles-engine"],
"matchPackagePrefixes": ["tsparticles-preset-"]
}
]
}

View File

@ -8,9 +8,9 @@ cd "$(dirname "$0")/.."
# Install/upgrade node when inside devcontainer
if [[ -n "$DEVCONTAINER" ]]; then
nodeCurrent=$(nvm version default || echo "")
nodeCurrent=$(nvm version default || :)
nodeLatest=$(nvm version-remote "$(cat .nvmrc)")
if [[ -z "$nodeCurrent" ]]; then
if [[ -z "$nodeCurrent" || "$nodeCurrent" == "N/A" ]]; then
nvm install
elif [[ "$nodeCurrent" != "$nodeLatest" ]]; then
nvm install --reinstall-packages-from="$nodeCurrent" --default

View File

@ -1,2 +0,0 @@
# Setuptools v62.3 doesn't support editable installs with just 'pyproject.toml' (PEP 660).
# Keep this file until it does!

View File

@ -33,6 +33,7 @@ import {
mdiGoogleCirclesCommunities,
mdiHomeAssistant,
mdiHomeAutomation,
mdiImage,
mdiImageFilterFrames,
mdiLightbulb,
mdiLightningBolt,
@ -90,6 +91,7 @@ export const FIXED_DOMAIN_ICONS = {
group: mdiGoogleCirclesCommunities,
homeassistant: mdiHomeAssistant,
homekit: mdiHomeAutomation,
image: mdiImage,
image_processing: mdiImageFilterFrames,
input_button: mdiGestureTapButton,
input_datetime: mdiCalendarClock,
@ -180,6 +182,7 @@ export const DOMAINS_WITH_CARD = [
"input_select",
"input_number",
"input_text",
"humidifier",
"lock",
"media_player",
"number",

View File

@ -1,4 +1,5 @@
import { isSameDay, isSameYear } from "date-fns";
import { HassConfig } from "home-assistant-js-websocket";
import { FrontendLocaleData } from "../../data/translation";
import {
formatShortDateTime,
@ -9,15 +10,16 @@ import { formatTime } from "./format_time";
export const absoluteTime = (
from: Date,
locale: FrontendLocaleData,
config: HassConfig,
to?: Date
): string => {
const _to = to ?? new Date();
if (isSameDay(from, _to)) {
return formatTime(from, locale);
return formatTime(from, locale, config);
}
if (isSameYear(from, _to)) {
return formatShortDateTime(from, locale);
return formatShortDateTime(from, locale, config);
}
return formatShortDateTimeWithYear(from, locale);
return formatShortDateTimeWithYear(from, locale, config);
};

View 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);

View File

@ -1,3 +1,4 @@
import { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { FrontendLocaleData, DateFormat } from "../../data/translation";
import "../../resources/intl-polyfill";
@ -5,37 +6,44 @@ import "../../resources/intl-polyfill";
// Tuesday, August 10
export const formatDateWeekdayDay = (
dateObj: Date,
locale: FrontendLocaleData
) => formatDateWeekdayDayMem(locale).format(dateObj);
locale: FrontendLocaleData,
config: HassConfig
) => formatDateWeekdayDayMem(locale, config.time_zone).format(dateObj);
const formatDateWeekdayDayMem = memoizeOne(
(locale: FrontendLocaleData) =>
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
weekday: "long",
month: "long",
day: "numeric",
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
})
);
// August 10, 2021
export const formatDate = (dateObj: Date, locale: FrontendLocaleData) =>
formatDateMem(locale).format(dateObj);
export const formatDate = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) => formatDateMem(locale, config.time_zone).format(dateObj);
const formatDateMem = memoizeOne(
(locale: FrontendLocaleData) =>
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "long",
day: "numeric",
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
})
);
// 10/08/2021
export const formatDateNumeric = (
dateObj: Date,
locale: FrontendLocaleData
locale: FrontendLocaleData,
config: HassConfig
) => {
const formatter = formatDateNumericMem(locale);
const formatter = formatDateNumericMem(locale, config.time_zone);
if (
locale.date_format === DateFormat.language ||
@ -67,7 +75,8 @@ export const formatDateNumeric = (
return formats[locale.date_format];
};
const formatDateNumericMem = memoizeOne((locale: FrontendLocaleData) => {
const formatDateNumericMem = memoizeOne(
(locale: FrontendLocaleData, serverTimeZone: string) => {
const localeString =
locale.date_format === DateFormat.system ? undefined : locale.language;
@ -79,6 +88,7 @@ const formatDateNumericMem = memoizeOne((locale: FrontendLocaleData) => {
year: "numeric",
month: "numeric",
day: "numeric",
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
});
}
@ -86,64 +96,99 @@ const formatDateNumericMem = memoizeOne((locale: FrontendLocaleData) => {
year: "numeric",
month: "numeric",
day: "numeric",
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
});
});
}
);
// Aug 10
export const formatDateShort = (dateObj: Date, locale: FrontendLocaleData) =>
formatDateShortMem(locale).format(dateObj);
export const formatDateShort = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) => formatDateShortMem(locale, config.time_zone).format(dateObj);
const formatDateShortMem = memoizeOne(
(locale: FrontendLocaleData) =>
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
day: "numeric",
month: "short",
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
})
);
// August 2021
export const formatDateMonthYear = (
dateObj: Date,
locale: FrontendLocaleData
) => formatDateMonthYearMem(locale).format(dateObj);
locale: FrontendLocaleData,
config: HassConfig
) => formatDateMonthYearMem(locale, config.time_zone).format(dateObj);
const formatDateMonthYearMem = memoizeOne(
(locale: FrontendLocaleData) =>
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
month: "long",
year: "numeric",
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
})
);
// August
export const formatDateMonth = (dateObj: Date, locale: FrontendLocaleData) =>
formatDateMonthMem(locale).format(dateObj);
export const formatDateMonth = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) => formatDateMonthMem(locale, config.time_zone).format(dateObj);
const formatDateMonthMem = memoizeOne(
(locale: FrontendLocaleData) =>
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
month: "long",
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
})
);
// 2021
export const formatDateYear = (dateObj: Date, locale: FrontendLocaleData) =>
formatDateYearMem(locale).format(dateObj);
export const formatDateYear = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) => formatDateYearMem(locale, config.time_zone).format(dateObj);
const formatDateYearMem = memoizeOne(
(locale: FrontendLocaleData) =>
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
})
);
// Monday
export const formatDateWeekday = (dateObj: Date, locale: FrontendLocaleData) =>
formatDateWeekdayMem(locale).format(dateObj);
export const formatDateWeekday = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) => formatDateWeekdayMem(locale, config.time_zone).format(dateObj);
const formatDateWeekdayMem = memoizeOne(
(locale: FrontendLocaleData) =>
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
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,
})
);

View File

@ -1,102 +1,99 @@
import { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
import "../../resources/intl-polyfill";
import { useAmPm } from "./use_am_pm";
import { formatDateNumeric } from "./format_date";
import { formatTime } from "./format_time";
import { useAmPm } from "./use_am_pm";
// August 9, 2021, 8:23 AM
export const formatDateTime = (dateObj: Date, locale: FrontendLocaleData) =>
formatDateTimeMem(locale).format(dateObj);
export const formatDateTime = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) => formatDateTimeMem(locale, config.time_zone).format(dateObj);
const formatDateTimeMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "long",
day: "numeric",
hour: useAmPm(locale) ? "numeric" : "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
export const formatShortDateTimeWithYear = (
dateObj: Date,
locale: FrontendLocaleData
) => formatShortDateTimeWithYearMem(locale).format(dateObj);
locale: FrontendLocaleData,
config: HassConfig
) => formatShortDateTimeWithYearMem(locale, config.time_zone).format(dateObj);
const formatShortDateTimeWithYearMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "short",
day: "numeric",
hour: useAmPm(locale) ? "numeric" : "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
export const formatShortDateTime = (
dateObj: Date,
locale: FrontendLocaleData
) => formatShortDateTimeMem(locale).format(dateObj);
locale: FrontendLocaleData,
config: HassConfig
) => formatShortDateTimeMem(locale, config.time_zone).format(dateObj);
const formatShortDateTimeMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
month: "short",
day: "numeric",
hour: useAmPm(locale) ? "numeric" : "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
export const formatDateTimeWithSeconds = (
dateObj: Date,
locale: FrontendLocaleData
) => formatDateTimeWithSecondsMem(locale).format(dateObj);
locale: FrontendLocaleData,
config: HassConfig
) => formatDateTimeWithSecondsMem(locale, config.time_zone).format(dateObj);
const formatDateTimeWithSecondsMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "long",
day: "numeric",
hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "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
export const formatDateTimeNumeric = (
dateObj: Date,
locale: FrontendLocaleData
) => `${formatDateNumeric(dateObj, locale)}, ${formatTime(dateObj, locale)}`;
locale: FrontendLocaleData,
config: HassConfig
) =>
`${formatDateNumeric(dateObj, locale, config)}, ${formatTime(
dateObj,
locale,
config
)}`;

View File

@ -1,76 +1,76 @@
import { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
import "../../resources/intl-polyfill";
import { useAmPm } from "./use_am_pm";
// 9:15 PM || 21:15
export const formatTime = (dateObj: Date, locale: FrontendLocaleData) =>
formatTimeMem(locale).format(dateObj);
export const formatTime = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) => formatTimeMem(locale, config.time_zone).format(dateObj);
const formatTimeMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
hour: "numeric",
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
export const formatTimeWithSeconds = (
dateObj: Date,
locale: FrontendLocaleData
) => formatTimeWithSecondsMem(locale).format(dateObj);
locale: FrontendLocaleData,
config: HassConfig
) => formatTimeWithSecondsMem(locale, config.time_zone).format(dateObj);
const formatTimeWithSecondsMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "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
export const formatTimeWeekday = (dateObj: Date, locale: FrontendLocaleData) =>
formatTimeWeekdayMem(locale).format(dateObj);
export const formatTimeWeekday = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) => formatTimeWeekdayMem(locale, config.time_zone).format(dateObj);
const formatTimeWeekdayMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
weekday: "long",
hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit",
hour12: useAmPm(locale),
}
)
hourCycle: useAmPm(locale) ? "h12" : "h23",
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
})
);
// 21:15
export const formatTime24h = (dateObj: Date) =>
formatTime24hMem().format(dateObj);
export const formatTime24h = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) => formatTime24hMem(locale, config.time_zone).format(dateObj);
const formatTime24hMem = memoizeOne(
() =>
(locale: FrontendLocaleData, serverTimeZone: string) =>
// en-GB to fix Chrome 24:59 to 0:59 https://stackoverflow.com/a/60898146
new Intl.DateTimeFormat("en-GB", {
hour: "numeric",
minute: "2-digit",
hour12: false,
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
})
);

View File

@ -8,8 +8,10 @@ export const useAmPm = memoizeOne((locale: FrontendLocaleData): boolean => {
) {
const testLanguage =
locale.time_format === TimeFormat.language ? locale.language : undefined;
const test = new Date().toLocaleString(testLanguage);
return test.includes("AM") || test.includes("PM");
const test = new Date("January 1, 2023 22:00:00").toLocaleString(
testLanguage
);
return test.includes("10");
}
return locale.time_format === TimeFormat.am_pm;

View File

@ -1,13 +1,15 @@
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";
type Callback = (oldValue: any, newValue: any) => void;
class Storage {
constructor(subscribe = true, storage = window.localStorage) {
class StorageClass {
constructor(storage = window.localStorage) {
this.storage = storage;
if (!subscribe) {
if (storage !== window.localStorage) {
// storage events only work for localStorage
return;
}
window.addEventListener("storage", (ev: StorageEvent) => {
@ -77,6 +79,7 @@ class Storage {
}
public setValue(storageKey: string, value: any): any {
const oldValue = this._storage[storageKey];
this._storage[storageKey] = value;
try {
if (value === undefined) {
@ -86,49 +89,68 @@ class Storage {
}
} catch (err: any) {
// 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 =
(
storageKey?: string,
property?: boolean,
subscribe = true,
storageType?: globalThis.Storage,
propertyOptions?: PropertyDeclaration
): any =>
export const storage =
(options: {
key?: string;
storage?: "localStorage" | "sessionStorage";
subscribe?: boolean;
state?: boolean;
stateOptions?: InternalPropertyDeclaration;
}): any =>
(clsElement: ClassElement) => {
const storage =
subscribe && !storageType
? subscribeStorage
: new Storage(subscribe, storageType);
const storageName = options.storage || "localStorage";
let storageInstance: StorageClass;
if (storageName && storageName in storages) {
storageInstance = storages[storageName];
} else {
storageInstance = new StorageClass(window[storageName]);
storages[storageName] = storageInstance;
}
const key = String(clsElement.key);
storageKey = storageKey || String(clsElement.key);
const storageKey = options.key || String(clsElement.key);
const initVal = clsElement.initializer
? clsElement.initializer()
: undefined;
storage.addFromStorage(storageKey);
storageInstance.addFromStorage(storageKey);
const subscribeChanges = (el: ReactiveElement): UnsubscribeFunc =>
storage.subscribeChanges(storageKey!, (oldValue) => {
const subscribeChanges =
options.subscribe !== false
? (el: ReactiveElement): UnsubscribeFunc =>
storageInstance.subscribeChanges(
storageKey!,
(oldValue, _newValue) => {
el.requestUpdate(clsElement.key, oldValue);
});
}
)
: undefined;
const getValue = (): any =>
storage.hasKey(storageKey!) ? storage.getValue(storageKey!) : initVal;
storageInstance.hasKey(storageKey!)
? storageInstance.getValue(storageKey!)
: initVal;
const setValue = (el: ReactiveElement, value: any) => {
let oldValue: unknown | undefined;
if (property) {
if (options.state) {
oldValue = getValue();
}
storage.setValue(storageKey!, value);
if (property) {
storageInstance.setValue(storageKey!, value);
if (options.state) {
el.requestUpdate(clsElement.key, oldValue);
}
};
@ -148,22 +170,23 @@ export const LocalStorage =
configurable: true,
},
finisher(cls: typeof ReactiveElement) {
if (property && subscribe) {
if (options.state && options.subscribe) {
const connectedCallback = cls.prototype.connectedCallback;
const disconnectedCallback = cls.prototype.disconnectedCallback;
cls.prototype.connectedCallback = function () {
connectedCallback.call(this);
this[`__unbsubLocalStorage${key}`] = subscribeChanges(this);
this[`__unbsubLocalStorage${key}`] = subscribeChanges?.(this);
};
cls.prototype.disconnectedCallback = function () {
disconnectedCallback.call(this);
this[`__unbsubLocalStorage${key}`]();
this[`__unbsubLocalStorage${key}`]?.();
this[`__unbsubLocalStorage${key}`] = undefined;
};
}
if (property) {
if (options.state) {
cls.createProperty(clsElement.key, {
noAccessor: true,
...propertyOptions,
...options.stateOptions,
});
}
},

View File

@ -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 { until } from "lit/directives/until";
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
@ -20,6 +20,7 @@ export const computeAttributeValueDisplay = (
localize: LocalizeFunc,
stateObj: HassEntity,
locale: FrontendLocaleData,
config: HassConfig,
entities: HomeAssistant["entities"],
attribute: string,
value?: any
@ -59,14 +60,14 @@ export const computeAttributeValueDisplay = (
if (isTimestamp(attributeValue)) {
const date = new Date(attributeValue);
if (checkValidDate(date)) {
return formatDateTimeWithSeconds(date, locale);
return formatDateTimeWithSeconds(date, locale, config);
}
}
// Value was not a timestamp, so only do date formatting
const date = new Date(attributeValue);
if (checkValidDate(date)) {
return formatDate(date, locale);
return formatDate(date, locale, config);
}
}
}
@ -92,6 +93,7 @@ export const computeAttributeValueDisplay = (
localize,
stateObj,
locale,
config,
entities,
attribute,
item

View File

@ -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 { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { FrontendLocaleData } from "../../data/translation";
import { FrontendLocaleData, TimeZone } from "../../data/translation";
import {
updateIsInstallingFromAttributes,
UPDATE_SUPPORT_PROGRESS,
@ -28,12 +28,14 @@ export const computeStateDisplaySingleEntity = (
localize: LocalizeFunc,
stateObj: HassEntity,
locale: FrontendLocaleData,
config: HassConfig,
entity: EntityRegistryDisplayEntry | undefined,
state?: string
): string =>
computeStateDisplayFromEntityAttributes(
localize,
locale,
config,
entity,
stateObj.entity_id,
stateObj.attributes,
@ -44,6 +46,7 @@ export const computeStateDisplay = (
localize: LocalizeFunc,
stateObj: HassEntity,
locale: FrontendLocaleData,
config: HassConfig,
entities: HomeAssistant["entities"],
state?: string
): string => {
@ -54,6 +57,7 @@ export const computeStateDisplay = (
return computeStateDisplayFromEntityAttributes(
localize,
locale,
config,
entity,
stateObj.entity_id,
stateObj.attributes,
@ -64,6 +68,7 @@ export const computeStateDisplay = (
export const computeStateDisplayFromEntityAttributes = (
localize: LocalizeFunc,
locale: FrontendLocaleData,
config: HassConfig,
entity: EntityRegistryDisplayEntry | undefined,
entityId: string,
attributes: any,
@ -119,29 +124,40 @@ export const computeStateDisplayFromEntityAttributes = (
if (domain === "datetime") {
const time = new Date(state);
return formatDateTime(time, locale);
return formatDateTime(time, locale, config);
}
if (["date", "input_datetime", "time"].includes(domain)) {
// 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`.
// These are timezone agnostic, so we should NOT use the system timezone here.
try {
const components = state.split(" ");
if (components.length === 2) {
// 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 (state.includes("-")) {
// 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(":")) {
// Time only.
const now = new Date();
return formatTime(
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`
if (
domain === "counter" ||
@ -175,11 +185,13 @@ export const computeStateDisplayFromEntityAttributes = (
// state is a timestamp
if (
["button", "input_button", "scene", "stt", "tts"].includes(domain) ||
["button", "image", "input_button", "scene", "stt", "tts"].includes(
domain
) ||
(domain === "sensor" && attributes.device_class === "timestamp")
) {
try {
return formatDateTime(new Date(state), locale);
return formatDateTime(new Date(state), locale, config);
} catch (_err) {
return state;
}

View File

@ -15,6 +15,7 @@ import {
mdiCheckCircleOutline,
mdiClock,
mdiCloseCircleOutline,
mdiCrosshairsQuestion,
mdiFan,
mdiFanOff,
mdiGestureTapButton,
@ -31,6 +32,7 @@ import {
mdiPowerPlugOff,
mdiRestart,
mdiRobot,
mdiRobotConfused,
mdiRobotOff,
mdiSpeaker,
mdiSpeakerOff,
@ -91,13 +93,19 @@ export const domainIconWithoutDefault = (
return alarmPanelIcon(compareState);
case "automation":
return compareState === "off" ? mdiRobotOff : mdiRobot;
return compareState === "unavailable"
? mdiRobotConfused
: compareState === "off"
? mdiRobotOff
: mdiRobot;
case "binary_sensor":
return binarySensorIcon(compareState, stateObj);
case "button":
switch (stateObj?.attributes.device_class) {
case "identify":
return mdiCrosshairsQuestion;
case "restart":
return mdiRestart;
case "update":

View File

@ -30,6 +30,7 @@ export const FIXED_DOMAIN_STATES = {
lock: ["jammed", "locked", "locking", "unlocked", "unlocking"],
media_player: ["idle", "off", "paused", "playing", "standby"],
person: ["home", "not_home"],
plant: ["ok", "problem"],
remote: ["on", "off"],
scene: [],
schedule: ["on", "off"],
@ -102,7 +103,15 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
frontend_stream_type: ["hls", "web_rtc"],
},
climate: {
hvac_action: ["off", "idle", "heating", "cooling", "drying", "fan"],
hvac_action: [
"off",
"idle",
"preheating",
"heating",
"cooling",
"drying",
"fan",
],
},
cover: {
device_class: [
@ -126,6 +135,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
},
humidifier: {
device_class: ["humidifier", "dehumidifier"],
action: ["off", "idle", "humidifying", "drying"],
},
media_player: {
device_class: ["tv", "speaker", "receiver"],

View File

@ -110,3 +110,15 @@ export const stateColorProperties = (
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 "";
};

View File

@ -17,7 +17,7 @@ export const stripPrefixFromEntityName = (
if (lowerCasedEntityName.startsWith(lowerCasedPrefixWithSuffix)) {
const newName = entityName.substring(lowerCasedPrefixWithSuffix.length);
if (newName.length) {
// If first word already has an upper case letter (e.g. from brand name)
// leave as-is, otherwise capitalize the first word.
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
@ -25,6 +25,7 @@ export const stripPrefixFromEntityName = (
: newName[0].toUpperCase() + newName.slice(1);
}
}
}
return undefined;
};

View 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);

View File

@ -1,10 +1,12 @@
import { addDays, startOfWeek } from "date-fns";
import { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
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) =>
formatDateWeekday(addDays(startOfWeek(new Date()), d), locale)
formatDateWeekday(addDays(startOfWeek(new Date()), d), locale, config)
)
);

View File

@ -1,10 +1,12 @@
import { addMonths, startOfYear } from "date-fns";
import { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
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) =>
formatDateMonth(addMonths(startOfYear(new Date()), m), locale)
formatDateMonth(addMonths(startOfYear(new Date()), m), locale, config)
)
);

View File

@ -80,33 +80,89 @@ _adapters._date.override({
format: function (time, fmt: keyof typeof FORMATS) {
switch (fmt) {
case "datetime":
return formatDateTime(new Date(time), this.options.locale);
return formatDateTime(
new Date(time),
this.options.locale,
this.options.config
);
case "datetimeseconds":
return formatDateTimeWithSeconds(new Date(time), this.options.locale);
return formatDateTimeWithSeconds(
new Date(time),
this.options.locale,
this.options.config
);
case "millisecond":
return formatTimeWithSeconds(new Date(time), this.options.locale);
return formatTimeWithSeconds(
new Date(time),
this.options.locale,
this.options.config
);
case "second":
return formatTimeWithSeconds(new Date(time), this.options.locale);
return formatTimeWithSeconds(
new Date(time),
this.options.locale,
this.options.config
);
case "minute":
return formatTime(new Date(time), this.options.locale);
return formatTime(
new Date(time),
this.options.locale,
this.options.config
);
case "hour":
return formatTime(new Date(time), this.options.locale);
return formatTime(
new Date(time),
this.options.locale,
this.options.config
);
case "weekday":
return formatDateWeekdayDay(new Date(time), this.options.locale);
return formatDateWeekdayDay(
new Date(time),
this.options.locale,
this.options.config
);
case "date":
return formatDate(new Date(time), this.options.locale);
return formatDate(
new Date(time),
this.options.locale,
this.options.config
);
case "day":
return formatDateShort(new Date(time), this.options.locale);
return formatDateShort(
new Date(time),
this.options.locale,
this.options.config
);
case "week":
return formatDate(new Date(time), this.options.locale);
return formatDate(
new Date(time),
this.options.locale,
this.options.config
);
case "month":
return formatDateMonth(new Date(time), this.options.locale);
return formatDateMonth(
new Date(time),
this.options.locale,
this.options.config
);
case "monthyear":
return formatDateMonthYear(new Date(time), this.options.locale);
return formatDateMonthYear(
new Date(time),
this.options.locale,
this.options.config
);
case "quarter":
return formatDate(new Date(time), this.options.locale);
return formatDate(
new Date(time),
this.options.locale,
this.options.config
);
case "year":
return formatDateYear(new Date(time), this.options.locale);
return formatDateYear(
new Date(time),
this.options.locale,
this.options.config
);
default:
return "";
}

View File

@ -30,6 +30,8 @@ class StateHistoryChartLine extends LitElement {
@property({ type: Boolean }) public showNames = true;
@property({ attribute: false }) public startTime!: Date;
@property({ attribute: false }) public endTime!: Date;
@property({ type: Number }) public paddingYAxis = 0;
@ -57,7 +59,12 @@ class StateHistoryChartLine extends LitElement {
}
public willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated || changedProps.has("showNames")) {
if (
!this.hasUpdated ||
changedProps.has("showNames") ||
changedProps.has("startTime") ||
changedProps.has("endTime")
) {
this._chartOptions = {
parsing: false,
animation: false,
@ -71,8 +78,10 @@ class StateHistoryChartLine extends LitElement {
adapters: {
date: {
locale: this.hass.locale,
config: this.hass.config,
},
},
suggestedMin: this.startTime,
suggestedMax: this.endTime,
ticks: {
maxRotation: 0,
@ -145,6 +154,8 @@ class StateHistoryChartLine extends LitElement {
}
if (
changedProps.has("data") ||
changedProps.has("startTime") ||
changedProps.has("endTime") ||
this._chartTime <
new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES)
) {
@ -375,6 +386,9 @@ class StateHistoryChartLine extends LitElement {
lastNullDate = date;
}
});
if (lastNullDate !== null) {
pushData(lastNullDate, [null]);
}
}
// Add an entry for final values

View File

@ -98,6 +98,7 @@ export class StateHistoryChartTimeline extends LitElement {
adapters: {
date: {
locale: this.hass.locale,
config: this.hass.config,
},
},
suggestedMin: this.startTime,
@ -181,8 +182,16 @@ export class StateHistoryChartTimeline extends LitElement {
return [
d.label || "",
formatDateTimeWithSeconds(d.start, this.hass.locale),
formatDateTimeWithSeconds(d.end, this.hass.locale),
formatDateTimeWithSeconds(
d.start,
this.hass.locale,
this.hass.config
),
formatDateTimeWithSeconds(
d.end,
this.hass.locale,
this.hass.config
),
formattedDuration,
];
},

View File

@ -52,8 +52,12 @@ export class StateHistoryCharts extends LitElement {
@property({ attribute: false }) public endTime?: Date;
@property({ attribute: false }) public startTime?: Date;
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
@property() public hoursToShow?: number;
@property({ type: Boolean }) public showNames = true;
@property({ type: Boolean }) public isLoadingData = false;
@ -95,13 +99,24 @@ export class StateHistoryCharts extends LitElement {
this._computedEndTime =
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.historyData.timeline.reduce(
(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()
)
);
}
const combinedItems = this.historyData.timeline.length
? (this.virtualize
@ -142,6 +157,7 @@ export class StateHistoryCharts extends LitElement {
.data=${item.data}
.identifier=${item.identifier}
.showNames=${this.showNames}
.startTime=${this._computedStartTime}
.endTime=${this._computedEndTime}
.paddingYAxis=${this._maxYWidth}
.names=${this.names}

View File

@ -146,6 +146,7 @@ class StatisticsChart extends LitElement {
adapters: {
date: {
locale: this.hass.locale,
config: this.hass.config,
},
},
ticks: {
@ -165,7 +166,7 @@ class StatisticsChart extends LitElement {
},
},
y: {
beginAtZero: false,
beginAtZero: this.chartType === "bar",
ticks: {
maxTicksLimit: 7,
},

View File

@ -349,6 +349,7 @@ export class HaDataTable extends LitElement {
class="mdc-data-table__content scroller ha-scrollbar"
@scroll=${this._saveScrollPos}
.items=${this._items}
.keyFunction=${this._keyFunction}
.renderItem=${this._renderRow}
></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) => {
// not sure how this happens...
if (!row) {

View File

@ -1,5 +1,5 @@
import { Remote, wrap } from "comlink";
import type { Api } from "./sort_filter_worker";
import type { Api } from "./sort-filter-worker";
type FilterDataType = Api["filterData"];
type FilterDataParamTypes = Parameters<FilterDataType>;
@ -9,27 +9,28 @@ type SortDataParamTypes = Parameters<SortDataType>;
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 = (
data: FilterDataParamTypes[0],
columns: FilterDataParamTypes[1],
filter: FilterDataParamTypes[2]
): Promise<ReturnType<FilterDataType>> => {
if (!worker) {
worker = wrap(new Worker(new URL("./sort_filter_worker", import.meta.url)));
}
return worker.filterData(data, columns, filter);
};
): Promise<ReturnType<FilterDataType>> =>
getWorker().filterData(data, columns, filter);
export const sortData = (
data: SortDataParamTypes[0],
columns: SortDataParamTypes[1],
direction: SortDataParamTypes[2],
sortColumn: SortDataParamTypes[3]
): Promise<ReturnType<SortDataType>> => {
if (!worker) {
worker = wrap(new Worker(new URL("./sort_filter_worker", import.meta.url)));
}
return worker.sortData(data, columns, direction, sortColumn);
};
): Promise<ReturnType<SortDataType>> =>
getWorker().sortData(data, columns, direction, sortColumn);

View File

@ -26,6 +26,10 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { ValueChangedEvent, HomeAssistant } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import {
fuzzyFilterSort,
ScorableTextItem,
} from "../../common/string/filter/sequence-matching";
interface Device {
name: string;
@ -33,6 +37,8 @@ interface Device {
id: string;
}
type ScorableDevice = ScorableTextItem & Device;
export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry
) => boolean;
@ -119,13 +125,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
deviceFilter: this["deviceFilter"],
entityFilter: this["entityFilter"],
excludeDevices: this["excludeDevices"]
): Device[] => {
): ScorableDevice[] => {
if (!devices.length) {
return [
{
id: "no_devices",
area: "",
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]
? areaLookup[device.area_id].name
: this.hass.localize("ui.components.device-picker.no_area"),
strings: [device.name || ""],
}));
if (!outputDevices.length) {
return [
@ -242,6 +250,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
id: "no_devices",
area: "",
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 = true;
(this.comboBox as any).items = this._getDevices(
const devices = this._getDevices(
this.devices!,
this.areas!,
this.entities!,
@ -295,6 +304,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
this.entityFilter,
this.excludeDevices
);
this.comboBox.items = devices;
this.comboBox.filteredItems = devices;
}
}
@ -314,6 +325,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
item-label-path="name"
@opened-changed=${this._openedChanged}
@value-changed=${this._deviceChanged}
@filter-changed=${this._filterChanged}
></ha-combo-box>
`;
}
@ -322,6 +334,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
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>) {
ev.stopPropagation();
let newValue = ev.detail.value;

View File

@ -7,15 +7,19 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
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 "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-icon-button";
import "../ha-svg-icon";
import "./state-badge";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
interface HassEntityWithCachedName extends HassEntity {
interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
friendly_name: string;
}
@ -159,6 +163,7 @@ export class HaEntityPicker extends LitElement {
),
icon: "mdi:magnify",
},
strings: [],
},
];
}
@ -169,10 +174,14 @@ export class HaEntityPicker extends LitElement {
);
return entityIds
.map((key) => ({
.map((key) => {
const friendly_name = computeStateName(hass!.states[key]) || key;
return {
...hass!.states[key],
friendly_name: computeStateName(hass!.states[key]) || key,
}))
friendly_name,
strings: [key, friendly_name],
};
})
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
@ -201,10 +210,14 @@ export class HaEntityPicker extends LitElement {
}
states = entityIds
.map((key) => ({
.map((key) => {
const friendly_name = computeStateName(hass!.states[key]) || key;
return {
...hass!.states[key],
friendly_name: computeStateName(hass!.states[key]) || key,
}))
friendly_name,
strings: [key, friendly_name],
};
})
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
@ -260,6 +273,7 @@ export class HaEntityPicker extends LitElement {
),
icon: "mdi:magnify",
},
strings: [],
},
];
}
@ -293,7 +307,7 @@ export class HaEntityPicker extends LitElement {
this.excludeEntities
);
if (this._initedStates) {
(this.comboBox as any).filteredItems = this._states;
this.comboBox.filteredItems = this._states;
}
this._initedStates = true;
}
@ -340,12 +354,11 @@ export class HaEntityPicker extends LitElement {
}
private _filterChanged(ev: CustomEvent): void {
const target = ev.target as HaComboBox;
const filterString = ev.detail.value.toLowerCase();
(this.comboBox as any).filteredItems = this._states.filter(
(entityState) =>
entityState.entity_id.toLowerCase().includes(filterString) ||
computeStateName(entityState).toLowerCase().includes(filterString)
);
target.filteredItems = filterString.length
? fuzzyFilterSort<HassEntityWithCachedName>(filterString, this._states)
: this._states;
}
private _setValue(value: string) {

View File

@ -62,6 +62,7 @@ class HaEntityStatePicker extends LitElement {
this.hass.localize,
state,
this.hass.locale,
this.hass.config,
this.hass.entities,
key
)
@ -69,6 +70,7 @@ class HaEntityStatePicker extends LitElement {
this.hass.localize,
state,
this.hass.locale,
this.hass.config,
this.hass.entities,
this.attribute,
key

View File

@ -192,6 +192,7 @@ export class HaStateLabelBadge extends LitElement {
this.hass!.localize,
entityState,
this.hass!.locale,
this.hass!.config,
this.hass!.entities
);
}

View File

@ -13,7 +13,10 @@ import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../common/entity/compute_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 { cameraUrlWithWidthHeight } from "../../data/camera";
import { HVAC_ACTION_TO_MODE } from "../../data/climate";
@ -153,8 +156,7 @@ export class StateBadge extends LitElement {
// eslint-disable-next-line
console.warn(errorMessage);
}
// lowest brightness will be around 50% (that's pretty dark)
iconStyle.filter = `brightness(${(brightness + 245) / 5}%)`;
iconStyle.filter = stateColorBrightness(stateObj);
}
if (stateObj.attributes.hvac_action) {
const hvacAction = stateObj.attributes.hvac_action;

View File

@ -64,7 +64,11 @@ class HaAbsoluteTime extends ReactiveElement {
if (!this.datetime) {
this.innerHTML = this.hass.localize("ui.components.absolute_time.never");
} else {
this.innerHTML = absoluteTime(new Date(this.datetime), this.hass.locale);
this.innerHTML = absoluteTime(
new Date(this.datetime),
this.hass.locale,
this.hass.config
);
}
}
}

View File

@ -7,6 +7,10 @@ import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import {
fuzzyFilterSort,
ScorableTextItem,
} from "../common/string/filter/sequence-matching";
import {
AreaRegistryEntry,
createAreaRegistryEntry,
@ -28,6 +32,8 @@ import type { HaComboBox } from "./ha-combo-box";
import "./ha-icon-button";
import "./ha-svg-icon";
type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry;
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (
item
) => html`<mwc-list-item
@ -306,9 +312,12 @@ export class HaAreaPicker extends LitElement {
this.entityFilter,
this.noAdd,
this.excludeAreas
);
(this.comboBox as any).items = areas;
(this.comboBox as any).filteredItems = areas;
).map((area) => ({
...area,
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;
}
const filteredItems = this.comboBox.items?.filter((item) =>
item.name.toLowerCase().includes(filter!.toLowerCase())
const filteredItems = fuzzyFilterSort<ScorableAreaRegistryEntry>(
filter,
this.comboBox?.items || []
);
if (!this.noAdd && filteredItems?.length === 0) {
this._suggestion = filter;
@ -409,7 +419,7 @@ export class HaAreaPicker extends LitElement {
name,
});
const areas = [...Object.values(this.hass.areas), area];
(this.comboBox as any).filteredItems = this._getAreas(
this.comboBox.filteredItems = this._getAreas(
areas,
Object.values(this.hass.devices)!,
Object.values(this.hass.entities)!,

View File

@ -62,6 +62,7 @@ class HaAttributes extends LitElement {
this.hass.localize,
this.stateObj!,
this.hass.locale,
this.hass.config,
this.hass.entities,
attribute
)}

View File

@ -28,6 +28,7 @@ class HaClimateState extends LitElement {
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"preset_mode"
)}`
@ -136,6 +137,7 @@ class HaClimateState extends LitElement {
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities
);
@ -144,6 +146,7 @@ class HaClimateState extends LitElement {
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"hvac_action"
)} (${stateString})`

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

View File

@ -176,7 +176,7 @@ export class HaControlSlider extends LitElement {
this._mc = undefined;
}
this.removeEventListener("keydown", this._handleKeyDown);
this.removeEventListener("keyup", this._handleKeyDown);
this.removeEventListener("keyup", this._handleKeyUp);
}
private get _tenPercentStep() {

View File

@ -1,9 +1,11 @@
import { mdiCalendar } from "@mdi/js";
import { HassConfig } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { formatDateNumeric } from "../common/datetime/format_date";
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
import { formatDateNumeric } from "../common/datetime/format_date";
import { fireEvent } from "../common/dom/fire_event";
import { TimeZone } from "../data/translation";
import { HomeAssistant } from "../types";
import "./ha-svg-icon";
import "./ha-textfield";
@ -59,7 +61,11 @@ export class HaDateInput extends LitElement {
.value=${this.value
? formatDateNumeric(
new Date(`${this.value.split("T")[0]}T00:00:00`),
this.locale
{
...this.locale,
time_zone: TimeZone.local,
},
{} as HassConfig
)
: ""}
.required=${this.required}

View File

@ -3,6 +3,13 @@ import "@material/mwc-list/mwc-list";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiCalendar } from "@mdi/js";
import {
addDays,
endOfDay,
endOfWeek,
startOfDay,
startOfWeek,
} from "date-fns";
import {
css,
CSSResultGroup,
@ -12,10 +19,11 @@ import {
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { formatDateTime } from "../common/datetime/format_date_time";
import { formatDate } from "../common/datetime/format_date";
import { useAmPm } from "../common/datetime/use_am_pm";
import { calcDate } from "../common/datetime/calc_date";
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 { HomeAssistant } from "../types";
import "./date-range-picker";
@ -34,7 +42,7 @@ export class HaDateRangePicker extends LitElement {
@property() public endDate!: Date;
@property() public ranges?: DateRangePickerRanges;
@property() public ranges?: DateRangePickerRanges | false;
@property() public autoApply = false;
@ -46,6 +54,70 @@ export class HaDateRangePicker extends LitElement {
@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) {
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
@ -65,15 +137,19 @@ export class HaDateRangePicker extends LitElement {
twentyfour-hours=${this._hour24format}
start-date=${this.startDate}
end-date=${this.endDate}
?ranges=${this.ranges !== undefined}
?ranges=${this.ranges !== false}
first-day=${firstWeekdayIndex(this.hass.locale)}
>
<div slot="input" class="date-range-inputs">
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
<ha-textfield
.value=${this.timePicker
? formatDateTime(this.startDate, this.hass.locale)
: formatDate(this.startDate, this.hass.locale)}
? formatDateTime(
this.startDate,
this.hass.locale,
this.hass.config
)
: formatDate(this.startDate, this.hass.locale, this.hass.config)}
.label=${this.hass.localize(
"ui.components.date-range-picker.start_date"
)}
@ -83,8 +159,8 @@ export class HaDateRangePicker extends LitElement {
></ha-textfield>
<ha-textfield
.value=${this.timePicker
? formatDateTime(this.endDate, this.hass.locale)
: formatDate(this.endDate, this.hass.locale)}
? formatDateTime(this.endDate, this.hass.locale, this.hass.config)
: formatDate(this.endDate, this.hass.locale, this.hass.config)}
.label=${this.hass.localize(
"ui.components.date-range-picker.end_date"
)}

View File

@ -34,6 +34,8 @@ const getValue = (obj, item) =>
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")
export class HaForm extends LitElement implements HaFormElement {
@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 warning?: Record<string, string>;
@property({ type: Boolean }) public disabled = false;
@property() public computeError?: (schema: any, error) => string;
@property() public computeWarning?: (schema: any, warning) => string;
@property() public computeLabel?: (
schema: any,
data: HaFormDataContainer
@ -98,6 +104,7 @@ export class HaForm extends LitElement implements HaFormElement {
: ""}
${this.schema.map((item) => {
const error = getError(this.error, item);
const warning = getWarning(this.warning, item);
return html`
${error
@ -106,6 +113,12 @@ export class HaForm extends LitElement implements HaFormElement {
${this._computeError(error, item)}
</ha-alert>
`
: warning
? html`
<ha-alert own-margin alert-type="warning">
${this._computeWarning(warning, item)}
</ha-alert>
`
: ""}
${"selector" in item
? html`<ha-selector
@ -187,6 +200,13 @@ export class HaForm extends LitElement implements HaFormElement {
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 {
return css`
.root > * {

View File

@ -109,7 +109,8 @@ class HaHLSPlayer extends LitElement {
private async _startHls(): Promise<void> {
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) {
return;

View File

@ -186,9 +186,8 @@ class HaHsColorPicker extends LitElement {
}
if (changedProps.has("value")) {
if (
this.value !== undefined &&
(this._localValue?.[0] !== this.value[0] ||
this._localValue?.[1] !== this.value[1])
this._localValue?.[0] !== this.value?.[0] ||
this._localValue?.[1] !== this.value?.[1]
) {
this._resetPosition();
}
@ -243,7 +242,11 @@ class HaHsColorPicker extends LitElement {
}
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._localValue = this.value;
}
@ -373,17 +376,20 @@ class HaHsColorPicker extends LitElement {
return css`
:host {
display: block;
outline: none;
}
.container {
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
display: flex;
}
canvas {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 50%;
cursor: pointer;
}
svg {
position: absolute;

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

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

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

View File

@ -3,6 +3,8 @@ import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { renderMarkdown } from "../resources/render-markdown";
const _blockQuoteToAlert = { Note: "info", Warning: "warning" };
@customElement("ha-markdown-element")
class HaMarkdownElement extends ReactiveElement {
@property() public content?;
@ -65,6 +67,34 @@ class HaMarkdownElement extends ReactiveElement {
node.loading = "lazy";
}
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);
}
}
}
}

View File

@ -73,20 +73,25 @@ class HaMenuButton extends LitElement {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldNarrow =
changedProps.get("narrow") ||
(oldHass && oldHass.dockedSidebar === "always_hidden");
const newNarrow =
const oldHass = changedProps.has("hass")
? (changedProps.get("hass") as HomeAssistant | undefined)
: this.hass;
const oldNarrow = changedProps.has("narrow")
? (changedProps.get("narrow") as boolean | undefined)
: this.narrow;
const oldShowButton =
oldNarrow || oldHass?.dockedSidebar === "always_hidden";
const showButton =
this.narrow || this.hass.dockedSidebar === "always_hidden";
if (oldNarrow === newNarrow) {
if (oldShowButton === showButton) {
return;
}
this.style.display = newNarrow || this._alwaysVisible ? "initial" : "none";
this.style.display = showButton || this._alwaysVisible ? "initial" : "none";
if (!newNarrow) {
if (!showButton) {
if (this._unsubNotifications) {
this._unsubNotifications();
this._unsubNotifications = undefined;
@ -98,6 +103,9 @@ class HaMenuButton extends LitElement {
}
private _subscribeNotifications() {
if (this._unsubNotifications) {
throw new Error("Already subscribed");
}
this._unsubNotifications = subscribeNotifications(
this.hass.connection,
(notifications) => {

View File

@ -2,7 +2,7 @@ import { mdiImagePlus } from "@mdi/js";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
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 {
CropOptions,

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